package org

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"math"
	"net/http"
	"sort"
	"strconv"

	"github.com/dchest/uniuri"
	"go.uber.org/multierr"

	"github.com/pingcap-inc/clinic/pkg/auth/api_key"
	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/auth/utils"
	"github.com/pingcap-inc/clinic/pkg/pagination"
	"github.com/pingcap-inc/clinic/pkg/resource_limit"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/api/common"
	error2 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/db/model"
	"github.com/pingcap-inc/clinic/pkg/rbac"
	"github.com/pingcap-inc/clinic/pkg/stream"
)

// @Summary		Invite user
// @Tags			Organization
// @Description	invite user to join the organization
// @Accept			json
// @Param			x-csrf-token	header		string		true	"get value from login.ValidationResp response"
// @Param			invite			body		InvitesReq	true	"invite user"
// @Param			oid				path		string		true	"organization id"
// @Success		200				{object}	common.SuccessResp
// @Router			/orgs/{oid}/users/invite [post]
func (o *OrganizationService) Invite(c *gin.Context) {
	u := session.GetUser(c)
	if u == nil {
		_ = c.Error(error2.ErrNotFound.New("the user is not found"))
		return
	}

	oid, err := common.GetOrgIDFromAPIPath(c)
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("unknown organization id"))
		return
	}

	ok, err := o.rbac.Auth(u.UserID, u.Internal, rbac.User(), rbac.WriteActing(), oid)
	if err != nil || !ok {
		_ = c.Error(error2.ErrForbidden.New("invite user failed"))
		return
	}

	var req InvitesReq
	if err := c.BindJSON(&req); err != nil {
		_ = c.Error(error2.ErrBadRequest.New("body is required"))
		return
	}

	var errs error
	stream.OfSlice(req.Invites).Each(func(i Invite) {
		errs = multierr.Append(errs, invite(i, o.rbac, u, oid))
	})

	if errs != nil {
		_ = c.Error(error2.ErrBadRequest.New("%s", errs.Error()))
		return
	}

	c.JSON(http.StatusOK, common.SuccessResponse())
}

func invite(invite Invite, rbac rbac.Auth, _ *session.CtxUser, oid uint) error {
	inviteUser, err := model.GetUserByUserName(invite.UserName)
	if err != nil {
		log.Error("query user failed", zap.String("user", invite.UserName))
		return fmt.Errorf("the user %s is not valid", invite.UserName)
	}

	if inviteUser == nil {
		//_ = c.Error(error2.ErrBadRequest.New("the user is not existed"))
		return fmt.Errorf("the user %s is not valid", invite.UserName)
	}
	// TODO: remove rbac?
	if err := rbac.AttachRoleForUser(oid, inviteUser.ID, invite.RoleID); err != nil {
		log.Error("attach role to the user failed", zap.Error(err))
		return fmt.Errorf("attach role to the user %s failed", invite.UserName)
	}
	return nil
}

// @Summary		Delete api key
// @Tags			Api Key
// @Description	create api key
// @Accept			json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Param			id				path		int		true	"key id"
// @Success		200				{object}	ApiKeyResponse
// @Router			/orgs/{oid}/keys/{id} [delete]
func (o *OrganizationService) DeleteCredential(c *gin.Context) {
	u := session.GetUser(c)
	if u == nil {
		_ = c.Error(error2.ErrNotFound.New("the user is not found"))
		return
	}

	oid, err := common.GetOrgIDFromAPIPath(c)
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("unknown organization id"))
		return
	}

	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("invalid id"))
		return
	}

	cre, err := model.GetCredential(uint(id))
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("find credential failed"))
		return
	}

	if cre.OrgID != oid || cre.UserID != u.UserID {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	if err := model.DeleteCredential(uint(id)); err != nil {
		log.Error("delete token failed", zap.Uint("id", uint(id)), zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("delete token failed"))
		return
	}

	c.JSON(http.StatusOK, common.SuccessResponse())
}

// @Summary		Create api key
// @Tags			Api Key
// @Description	create api key
// @Accept			json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Success		200				{object}	ApiKeyResponse
// @Router			/orgs/{oid}/keys [post]
func (o *OrganizationService) AddCredential(c *gin.Context) {
	u := session.GetUser(c)
	if u == nil {
		_ = c.Error(error2.ErrNotFound.New("the user is not found"))
		return
	}

	oid, err := common.GetOrgIDFromAPIPath(c)
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("unknown organization id"))
		return
	}

	ok, err := o.rbac.Auth(u.UserID, u.Internal, rbac.Cluster(), rbac.WriteActing(), oid)
	if err != nil || !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	if !o.quotalimiter.AskForResource(resource_limit.CredentialQuota, 1, oid, u.UserID) {
		_ = c.Error(error2.ErrPreConditionFailed.New("%s", resource_limit.CredentialQuota.Reason()))
		return
	}

	//var val ApiKeyRequest
	//if err := c.BindJSON(&val); err != nil {
	//	_ = c.Error(error2.ErrBadRequest.New("body is required"))
	//	return
	//}
	//if val.Name == "" {
	//	_ = c.Error(error2.ErrBadRequest.New("bad parameter"))
	//	return
	//}

	jsonKey := api_key.ApiKeyJson{}
	jsonKey.OrgId = oid
	jsonKey.UserId = u.UserID
	jsonKey.Key = uniuri.New()

	hashedKey, err := utils.EncodePassword(jsonKey.Key, fmt.Sprintf("api_key%d", u.UserID))
	if err != nil {
		_ = c.Error(error2.ErrInternal.New("generate api key failed"))
		return
	}

	jsonString, err := json.Marshal(jsonKey)
	if err != nil {
		_ = c.Error(error2.ErrInternal.New("generate api key failed"))
		return
	}

	cre, err := model.AddCredential(oid, u.UserID, hashedKey)
	if err != nil {
		log.Error("add credential failed", zap.Uint("user", u.UserID), zap.Uint("org", oid), zap.Error(err))
		_ = c.Error(error2.ErrBadRequest.New("the name is existed"))
		return
	}

	c.JSON(http.StatusOK, ApiKeyResponse{
		ID: cre.ID,
		// Name: val.Name,
		Key: base64.StdEncoding.EncodeToString(jsonString),
	})
}

// @Summary		List upload key
// @Tags			Api Key
// @Description	list upload key of the user in current organizatioin
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Param			limit			query		int		false	"the count per page"
// @Param			page			query		int		false	"the page index"
// @Success		200				{object}	ListApiKeyResponse
// @Router			/orgs/{oid}/keys [get]
func (o *OrganizationService) ListCredentials(c *gin.Context) {
	u := session.GetUser(c)
	if u == nil {
		_ = c.Error(error2.ErrNotFound.New("the user is not found"))
		return
	}

	oid, err := common.GetOrgIDFromAPIPath(c)
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("unknown organization id"))
		return
	}

	ok, err := o.rbac.Auth(u.UserID, u.Internal, rbac.Cluster(), rbac.ReadActing(), oid)
	if err != nil || !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	pq := common.ParsePaginationQuery(c.Request.URL.RequestURI())
	credentials, total, err := model.ListCredentials(oid, u.UserID, pq.Page, pq.Limit)
	if err != nil {
		log.Error("qery credentials failed", zap.Uint("org", oid), zap.Uint("user", u.UserID), zap.Error(err))
		_ = c.Error(error2.ErrNotFound.New("query credentials failed"))
		return
	}

	var tokens []ApiKeyResponse
	stream.OfSlice(credentials).Map(func(cre db.Credential) ApiKeyResponse {
		return ApiKeyResponse{
			ID:          cre.ID,
			CreatedTime: cre.CreatedAt.Unix(),
			Key:         "******",
		}
	}).CollectEmptySlice(&tokens)

	c.JSON(http.StatusOK, ListApiKeyResponse{
		Total: total,
		Items: tokens,
	})
}

//	@Summary		List api key
//	@Tags			Api Key
//	@Description	list api key of the user in current organizatioin
//	@Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
//	@Param			oid				path		string	true	"organization id"
//	@Param			limit			query		int		false	"the count per page"
//	@Param			page			query		int		false	"the page index"
//	@Success		200				{object}	ListApiKeyResponse
//	@Router			/orgs/{oid}/keys [get]
//func (o *OrganizationService) ListAPICredentials(c *gin.Context) {
//	u := session.GetUser(c)
//	if u == nil {
//		_ = c.Error(error2.ErrNotFound.New("the user is not found"))
//		return
//	}
//
//	oid, err := common.GetOrgIDFromAPIPath(c)
//	if err != nil {
//		_ = c.Error(error2.ErrNotFound.New("unknown organization id"))
//		return
//	}
//
//	ok, err := o.rbac.Auth(u.UserID, u.Internal, rbac.Cluster(), rbac.ReadActing(), oid)
//	if err != nil || !ok {
//		_ = c.Error(error2.ErrForbidden.New("permission deny"))
//		return
//	}
//
//	pq := common.ParsePaginationQuery(c.Request.URL.RequestURI())
//	credentials, total, err := model.ListAPICredentials(oid, u.UserID, pq.Page, pq.Limit)
//	if err != nil {
//		log.Error("query credentials failed", zap.Uint("org", oid), zap.Uint("user", u.UserID), zap.Error(err))
//		_ = c.Error(error2.ErrNotFound.New("query credentials failed"))
//		return
//	}
//
//	var tokens []ApiKeyResponse
//	stream.OfSlice(credentials).Map(func(cre db.APICredential) ApiKeyResponse {
//		jsonKey := api_key.ApiKeyJson{}
//		jsonKey.OrgId = cre.OrgID
//		jsonKey.UserId = cre.UserID
//		jsonKey.Key = cre.PlainKey
//
//		var key string
//		jsonBytes, err := json.Marshal(jsonKey)
//		if err != nil {
//			log.Error("json marshal failed", zap.Uint("org", oid), zap.Uint("user", u.UserID), zap.Error(err))
//		} else {
//			key = base64.StdEncoding.EncodeToString(jsonBytes)
//		}
//		return ApiKeyResponse{
//			ID:          cre.ID,
//			CreatedTime: cre.CreatedAt.Unix(),
//			Key:         key,
//		}
//	}).CollectEmptySlice(&tokens)
//
//	c.JSON(http.StatusOK, ListApiKeyResponse{
//		Total: total,
//		Items: tokens,
//	})
//}

// @Summary		List users
// @Tags			Organization
// @Description	list users of the organization
// @Accept			json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			limit			query		int		false	"the count per page"
// @Param			page			query		int		false	"the page index"
// @Param			oid				path		string	true	"organization id"
// @Success		200				{object}	ListUsersResp
// @Router			/orgs/{oid}/users [get]
func (o *OrganizationService) ListUsers(c *gin.Context) {
	u := session.GetUser(c)
	if u == nil {
		_ = c.Error(error2.ErrNotFound.New("the user is not found"))
		return
	}
	oid, err := common.GetOrgIDFromAPIPath(c)
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("unknown organization id"))
		return
	}

	ok, err := o.rbac.Auth(u.UserID, u.Internal, rbac.User(), rbac.ReadActing(), oid)
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("check rbac failed"))
		return
	}

	if !ok {
		_ = c.Error(error2.ErrForbidden.New("permission deny"))
		return
	}

	results := make([]User, 0)
	// TODO: remove rbac
	ids := o.rbac.GetAllUsersInOrg(oid)

	sort.Slice(ids, func(i, j int) bool {
		return ids[i] > ids[j]
	})

	pq := common.ParsePaginationQuery(c.Request.URL.RequestURI())

	startIndex := pagination.GetStartIndex(pq.Page, pq.Limit)

	ids = ids[startIndex:int(math.Min(float64(startIndex+pq.Limit), float64(len(ids))))]
	stream.OfSlice(ids).Map(func(id uint) *db.User {
		u, err := model.GetUserByID(id)
		if err != nil {
			log.Error("query user failed", zap.Uint("user", u.ID), zap.Error(err))
			return nil
		}

		return u
	}).Filter(func(user *db.User) bool {
		return user != nil && u.UserID != user.ID
	}).Each(func(u *db.User) {
		us, err := o.buildUserRole(oid, u.ID, u.UserName)
		if err != nil {
			log.Error("build user  role failed", zap.Error(err))
			return
		}
		results = append(results, *us)
	})

	c.JSON(http.StatusOK, ListUsersResp{
		Total: len(results),
		Items: results,
	})
}

// TODO: remove rbac?
func (o *OrganizationService) buildUserRole(orgID, userID uint, userName string) (*User, error) {
	roles, err := model.GetRolesByIDs(o.rbac.GetRolesOfUserInOrg(userID, orgID), orgID)
	if err != nil {
		return nil, errors.New("get roles failed")
	}
	permission := 0
	rolename := ""
	for _, role := range roles {
		irolename, rolepermisson := common.InternalRoleToResponseRole(role.RoleName)
		if rolepermisson > permission {
			permission = rolepermisson
			rolename = irolename
		}
	}
	return &User{UserID: uint64(userID), UserName: userName, RoleName: rolename}, nil
}

// @Summary		Delete user from organization
// @Tags			Organization
// @Description	delete user from organization
// @Accept			json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Param			id				path		int		true	"user id"
// @Success		200				{object}	common.SuccessResp
// @Router			/orgs/{oid}/users/{id} [delete]
func (o *OrganizationService) DeleteUser(c *gin.Context) {
	u := session.GetUser(c)
	if u == nil {
		_ = c.Error(error2.ErrNotFound.New("the user is not found"))
		return
	}
	oid, err := common.GetOrgIDFromAPIPath(c)
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("unknown organization id"))
		return
	}

	ok, err := o.rbac.Auth(u.UserID, u.Internal, rbac.User(), rbac.WriteActing(), oid)
	if err != nil || !ok {
		log.Error("delete user rbac auth failed", zap.Uint("user_id", u.UserID), zap.Uint("oid", oid), zap.Error(err), zap.Bool("ok", ok))
		_ = c.Error(error2.ErrInternal.New("delete user failed"))
		return
	}

	deleteUser, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("bad parameter"))
		return
	}

	if err := o.rbac.DeleteUserInOrganization(oid, uint(deleteUser)); err != nil {
		log.Error("delete user from organization failed", zap.Uint("org", oid), zap.Uint("user", uint(deleteUser)), zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("delete user failed"))
		return
	}

	if err := model.DeleteCredentialByUserID(uint(deleteUser)); err != nil {
		log.Error("delete user credential failed", zap.Uint("user", uint(deleteUser)), zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("delete user failed"))
		return
	}

	c.JSON(http.StatusOK, common.SuccessResponse())
}

// @Summary		Update user role in organization
// @Tags			Organization
// @Description	update user role in organization
// @Accept			json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			oid				path		string	true	"organization id"
// @Param			id				path		int		true	"user id"
// @Param			role			body		Role	true	"user role"
// @Success		200				{object}	common.SuccessResp
// @Router			/orgs/{oid}/users/{id} [put]
func (o *OrganizationService) UpdateUser(c *gin.Context) {
	u := session.GetUser(c)
	if u == nil {
		_ = c.Error(error2.ErrNotFound.New("the user is not found"))
		return
	}

	var req Role
	if err := c.BindJSON(&req); err != nil {
		_ = c.Error(error2.ErrBadRequest.New("body is required"))
		return
	}

	oid, err := common.GetOrgIDFromAPIPath(c)
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("unknown organization id"))
		return
	}

	ok, err := o.rbac.Auth(u.UserID, u.Internal, rbac.User(), rbac.WriteActing(), oid)
	if err != nil || !ok {
		_ = c.Error(error2.ErrInternal.New("update user failed"))
		return
	}

	deleteUser, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("bad parameter"))
		return
	}

	// it should be a transaction
	if err := o.rbac.DeleteUserInOrganization(oid, uint(deleteUser)); err != nil {
		_ = c.Error(error2.ErrInternal.New("update user failed"))
		return
	}

	// TODO: Remove rbac
	if err := o.rbac.AttachRoleForUser(oid, uint(deleteUser), req.ID); err != nil {
		_ = c.Error(error2.ErrInternal.New("update user failed"))
		return
	}

	c.JSON(http.StatusOK, common.SuccessResponse())
}
