package tigin

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

	"github.com/tidbcloud/pkgv2/auth/kong"
	"github.com/tidbcloud/tiinf/sdk/rbac"

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"go.uber.org/zap"

	rbac_common "github.com/tidbcloud/tiinf/sdk/rbac/common"
	rbac_useadapter "github.com/tidbcloud/tiinf/sdk/rbac/useadapter"

	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
)

const (
	CURRENT_USER   = "current_user"
	AUTH_USER      = "auth_user"
	CURRENT_APIKEY = "current_apikey"
)

type AuthInfo struct {
	AuthType      string
	AccountUser   *account.User
	AuthUser      *account.GetAuthUserResponse
	OpenAPIAccess *account.GetOpenAPIAccessResp
}

func getAuthUser(ctx context.Context, accountClient account.AccountAPIServiceClient, orgId uint64, email string) (*account.GetAuthUserResponse, error) {
	resp, err := accountClient.GetAuthUser(ctx, &account.GetAuthUserRequest{OrgId: orgId, UserEmail: email, SkipCheckOrg: false})
	if err != nil {
		return nil, err
	}

	return resp, err
}

func getUserByToken(ctx context.Context, accountClient account.AccountAPIServiceClient, token string) (*account.User, error) {
	resp, err := accountClient.GetUserByToken(ctx, &account.GetUserByTokenRequest{Token: token})
	if err != nil {
		return nil, err
	}

	return resp.User, err
}

func isUserInOrg(ctx context.Context, accountClient account.AccountAPIServiceClient, userID uint64, orgID uint64) (bool, error) {
	resp, err := accountClient.ListOrgsByUser(ctx, &account.ListOrgsByUserRequest{UserId: userID})
	if err != nil {
		return false, err
	}

	for _, i := range resp.Orgs {
		if i.Id == orgID {
			return true, nil
		}
	}

	return false, nil
}

func isUserInProject(ctx context.Context, accountClient account.AccountAPIServiceClient, userID uint64, orgID uint64, projectID uint64) (bool, error) {
	resp, err := accountClient.ListProjectsByOrgsInUser(ctx, &account.ListProjectsByOrgsInUserRequest{
		UserId: userID, OrgIds: []uint64{orgID},
	})
	if err != nil {
		return false, err
	}

	for _, i := range resp.Projects {
		for _, j := range i.Projects {
			if j.Id == projectID {
				return true, nil
			}
		}
	}

	return false, nil
}

func GetUserIDForRBAC(ctx context.Context, param *rbac_common.AccessParam) (string, error) {
	c := param.Req.(*gin.Context)
	user, err := GetCurrentUser(c)
	if err != nil {
		return "", errs.NewNotFound("user not found", nil)
	}
	return strconv.FormatUint(user.Id, 10), nil
}

func CheckRBACPermission(accountClient account.AccountAPIServiceClient, resourceMap map[string]string, pathWhitelist []string) gin.HandlerFunc {
	return rbac_useadapter.GinPermissionCheckInterceptorWithCommonParam(&rbac.PermissionCheckerCommonParam{
		AccountClient:           accountClient,
		IdentityF:               GetUserIDForRBAC,
		PathPattern2ResourceMap: resourceMap,
		PathWhitelist:           pathWhitelist,
	})
}

func CheckCloudRBACPermission(accountClient account.AccountAPIServiceClient, resourceMap map[string]string, pathWhitelist []string) gin.HandlerFunc {
	return rbac_useadapter.GinCloudPermissionCheckInterceptorWithCommonParam(&rbac.PermissionCheckerCommonParam{
		AccountClient:           accountClient,
		IdentityF:               GetUserIDForRBAC,
		PathPattern2ResourceMap: resourceMap,
		PathWhitelist:           pathWhitelist,
	})
}

func CheckIsAPIRequest(c *gin.Context) (bool, error) {
	isApiKeyReq, _, err := isApiKeyRequest(c)
	return isApiKeyReq, err
}

func isApiKeyRequest(c *gin.Context) (bool, string, error) {
	authType := c.GetHeader("X-Auth-Type")
	if authType != string(kong.AuthTypeOpenAPI) {
		return false, "", nil
	}
	authContent := c.GetHeader("X-Auth-Content")
	var result kong.OpenAPIAuthResult
	err := json.Unmarshal([]byte(authContent), &result)
	if err != nil {
		return false, "", err
	}
	return true, result.PublicKey, nil
}

func CheckAuthByAuth0AndAPIKey(accountClient account.AccountAPIServiceClient) gin.HandlerFunc {
	return CheckUserAndPermission(accountClient)
}

func CheckAuthByAuth0(accountClient account.AccountAPIServiceClient) gin.HandlerFunc {
	return CheckUserAndPermission(accountClient)
}
func CheckUserAndPermission(accountClient account.AccountAPIServiceClient) gin.HandlerFunc {
	return func(c *gin.Context) {
		ctx := c.Request.Context()

		uriParams := URIParams{}
		if err := c.ShouldBindUri(&uriParams); err != nil {
			tilog.Info(ctx, "failed to bind uri params", zap.Error(err))
		}

		isApiKeyReq, apiKey, err := isApiKeyRequest(c)
		if err != nil {
			tilog.Error(ctx, "failed to check api key request", zap.Error(err))
			AbortWithError(c, errs.NewUnauthorized("unauthorized", nil))
			return
		}
		if isApiKeyReq {
			req := account.CheckIdentityAndPermissionRequest{
				IdentityType: account.RbacIdentityType_RBAC_IDENTITY_TYPE_APIKEY,
				Identity:     apiKey,
				ResourceUri:  c.Request.URL.Path,
			}
			rsp, err := accountClient.CheckIdentityAndPermission(ctx, &req)
			if err != nil {
				tilog.Error(ctx, "failed to check identity and permission", zap.Error(err))
				AbortWithError(c, errs.NewUnauthorized("unauthorized", nil))
				return
			}
			if rsp.CheckRet != 0 {
				AbortWithError(c, errs.NewUnauthorized("unauthorized", nil))
				return
			}
			accReq := account.GetOpenAPIAccessReq{
				PublicKey: apiKey,
			}
			accResp, err := accountClient.GetOpenAPIAccess(ctx, &accReq)
			if err != nil {
				tilog.Error(ctx, "failed to get open api access", zap.Error(err))
				AbortWithError(c, errs.NewUnauthorized("unauthorized", nil))
				return
			}

			c.Set(CURRENT_APIKEY, accResp)
			c.Next()
			return
		}

		token := c.GetHeader("Authorization")
		if token == "" {
			tilog.Info(ctx, "no header found with `Authorization`")
			AbortWithError(c, errs.NewUnauthorized("unauthorized", nil))
			return
		}
		items := strings.Split(token, " ")
		if len(items) != 2 {
			tilog.Info(ctx, "token not found with header `Authorization`")
			AbortWithError(c, errs.NewUnauthorized("unauthorized", nil))
			return
		}
		if strings.ToLower(items[0]) != "bearer" {
			tilog.Info(ctx, "token type mismatch with header `Authorization`", zap.String("type", items[0]))
			AbortWithError(c, errs.NewUnauthorized("unauthorized", nil))
			return
		}

		token = items[1]

		user, err := getUserByToken(ctx, accountClient, token)
		if err != nil || user == nil {
			tilog.Error(ctx, "failed to get user by token", zap.Error(err), zap.Any("user", user))
			AbortWithError(c, errs.NewUnauthorized("unauthorized", nil))
			return
		}

		if uriParams.OrgID != 0 {
			authUser, err := getAuthUser(ctx, accountClient, uriParams.OrgID, user.Email)
			if err != nil {
				AbortWithError(c, errs.NewUnauthorized("Fail to get Auth user info", nil))
			}
			c.Set(AUTH_USER, authUser)
		}

		// XXX(bing.wang) Since we already checked the user's permissions in rbac,
		//     we don't need to check whether the user belongs to the current org or project
		//		if uriParams.OrgID != 0 {
		//			exists, err := isUserInOrg(ctx, accountClient, user.Id, uriParams.OrgID)
		//			if err != nil {
		//				tilog.Error(ctx, "failed to check if user in org", zap.Error(err), zap.Any("user", user), zap.Any("uriParams", uriParams))
		//				AbortWithError(c, errs.NewForbidden("permission denied", nil))
		//				return
		//			}
		//
		//			if !exists {
		//				AbortWithError(c, errs.NewForbidden("permission denied", nil))
		//				return
		//			}
		//		}
		//
		//		if uriParams.ProjectID != 0 {
		//			exists, err := isUserInProject(ctx, accountClient, user.Id, uriParams.OrgID, uriParams.ProjectID)
		//			if err != nil {
		//				tilog.Error(ctx, "failed to check if user in project", zap.Error(err), zap.Any("user", user), zap.Any("uriParams", uriParams))
		//				AbortWithError(c, errs.NewForbidden("permission denied", nil))
		//				return
		//			}
		//
		//			if !exists {
		//				AbortWithError(c, errs.NewForbidden("permission denied", nil))
		//				return
		//			}
		//		}

		c.Set(CURRENT_USER, user)

		c.Next()
	}
}

func GetCurrentAuthInfo(c *gin.Context) (*AuthInfo, error) {
	authInfo := AuthInfo{}
	if isAPIKey, _ := CheckIsAPIRequest(c); isAPIKey {
		authInfo.AuthType = CURRENT_APIKEY
		resp, err := GetCurrentOpenAPIAccess(c)
		if err != nil {
			return nil, err
		}
		authInfo.OpenAPIAccess = resp
	} else {
		authInfo.AuthType = CURRENT_USER
		resp, err := GetCurrentUser(c)
		if err != nil {
			return nil, err
		}
		authInfo.AccountUser = resp
		authUser, err := GetAuthUser(c)
		if err != nil {
			return nil, err
		}
		authInfo.AuthUser = authUser
	}
	return &authInfo, nil
}

func GetCurrentOpenAPIAccess(c *gin.Context) (*account.GetOpenAPIAccessResp, error) {
	v, ok := c.Get(CURRENT_APIKEY)
	if !ok {
		return nil, errs.NewNotFound("user not found", nil)
	}

	return v.(*account.GetOpenAPIAccessResp), nil
}

func GetCurrentUser(c *gin.Context) (*account.User, error) {
	v, ok := c.Get(CURRENT_USER)
	if !ok {
		return nil, errs.NewNotFound("user not found", nil)
	}

	return v.(*account.User), nil
}

func GetAuthUser(c *gin.Context) (*account.GetAuthUserResponse, error) {
	v, ok := c.Get(AUTH_USER)
	if !ok {
		return nil, errs.NewNotFound("user not found", nil)
	}

	return v.(*account.GetAuthUserResponse), nil
}
