// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package restapi

import (
	"context"

	"github.com/go-openapi/errors"
	"github.com/go-openapi/runtime/middleware"
	"github.com/minio/console/pkg/utils"
	"github.com/minio/console/restapi/operations"
	"github.com/minio/madmin-go"

	groupApi "github.com/minio/console/restapi/operations/group"

	"github.com/minio/console/models"
)

func registerGroupsHandlers(api *operations.ConsoleAPI) {
	// List Groups
	api.GroupListGroupsHandler = groupApi.ListGroupsHandlerFunc(func(params groupApi.ListGroupsParams, session *models.Principal) middleware.Responder {
		listGroupsResponse, err := getListGroupsResponse(session, params)
		if err != nil {
			return groupApi.NewListGroupsDefault(int(err.Code)).WithPayload(err)
		}
		return groupApi.NewListGroupsOK().WithPayload(listGroupsResponse)
	})
	// Group Info
	api.GroupGroupInfoHandler = groupApi.GroupInfoHandlerFunc(func(params groupApi.GroupInfoParams, session *models.Principal) middleware.Responder {
		groupInfo, err := getGroupInfoResponse(session, params)
		if err != nil {
			return groupApi.NewGroupInfoDefault(int(err.Code)).WithPayload(err)
		}
		return groupApi.NewGroupInfoOK().WithPayload(groupInfo)
	})
	// Add Group
	api.GroupAddGroupHandler = groupApi.AddGroupHandlerFunc(func(params groupApi.AddGroupParams, session *models.Principal) middleware.Responder {
		if err := getAddGroupResponse(session, params); err != nil {
			return groupApi.NewAddGroupDefault(int(err.Code)).WithPayload(err)
		}
		return groupApi.NewAddGroupCreated()
	})
	// Remove Group
	api.GroupRemoveGroupHandler = groupApi.RemoveGroupHandlerFunc(func(params groupApi.RemoveGroupParams, session *models.Principal) middleware.Responder {
		if err := getRemoveGroupResponse(session, params); err != nil {
			return groupApi.NewRemoveGroupDefault(int(err.Code)).WithPayload(err)
		}
		return groupApi.NewRemoveGroupNoContent()
	})
	// Update Group
	api.GroupUpdateGroupHandler = groupApi.UpdateGroupHandlerFunc(func(params groupApi.UpdateGroupParams, session *models.Principal) middleware.Responder {
		groupUpdateResp, err := getUpdateGroupResponse(session, params)
		if err != nil {
			return groupApi.NewUpdateGroupDefault(int(err.Code)).WithPayload(err)
		}
		return groupApi.NewUpdateGroupOK().WithPayload(groupUpdateResp)
	})
}

// getListGroupsResponse performs listGroups() and serializes it to the handler's output
func getListGroupsResponse(session *models.Principal, params groupApi.ListGroupsParams) (*models.ListGroupsResponse, *models.Error) {
	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
	defer cancel()
	mAdmin, err := NewMinioAdminClient(session)
	if err != nil {
		return nil, ErrorWithContext(ctx, err)
	}
	// create a MinIO Admin Client interface implementation
	// defining the client to be used
	adminClient := AdminClient{Client: mAdmin}

	groups, err := adminClient.listGroups(ctx)
	if err != nil {
		return nil, ErrorWithContext(ctx, err)
	}

	// serialize output
	listGroupsResponse := &models.ListGroupsResponse{
		Groups: groups,
		Total:  int64(len(groups)),
	}

	return listGroupsResponse, nil
}

// groupInfo calls MinIO server get Group's info
func groupInfo(ctx context.Context, client MinioAdmin, group string) (*madmin.GroupDesc, error) {
	groupDesc, err := client.getGroupDescription(ctx, group)
	if err != nil {
		return nil, err
	}
	return groupDesc, nil
}

// getGroupInfoResponse performs groupInfo() and serializes it to the handler's output
func getGroupInfoResponse(session *models.Principal, params groupApi.GroupInfoParams) (*models.Group, *models.Error) {
	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
	defer cancel()
	mAdmin, err := NewMinioAdminClient(session)
	if err != nil {
		return nil, ErrorWithContext(ctx, err)
	}
	// create a MinIO Admin Client interface implementation
	// defining the client to be used
	adminClient := AdminClient{Client: mAdmin}

	groupName, err := utils.DecodeBase64(params.Name)
	if err != nil {
		return nil, ErrorWithContext(ctx, err)
	}

	groupDesc, err := groupInfo(ctx, adminClient, groupName)
	if err != nil {
		return nil, ErrorWithContext(ctx, err)
	}

	groupResponse := &models.Group{
		Members: groupDesc.Members,
		Name:    groupDesc.Name,
		Policy:  groupDesc.Policy,
		Status:  groupDesc.Status,
	}

	return groupResponse, nil
}

// addGroupAdd a MinIO group with the defined members
func addGroup(ctx context.Context, client MinioAdmin, group string, members []string) error {
	gAddRemove := madmin.GroupAddRemove{
		Group:    group,
		Members:  members,
		IsRemove: false,
	}
	err := client.updateGroupMembers(ctx, gAddRemove)
	if err != nil {
		return err
	}
	return nil
}

// getAddGroupResponse performs addGroup() and serializes it to the handler's output
func getAddGroupResponse(session *models.Principal, params groupApi.AddGroupParams) *models.Error {
	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
	defer cancel()
	// AddGroup request needed to proceed
	if params.Body == nil {
		return ErrorWithContext(ctx, ErrGroupBodyNotInRequest)
	}
	groupRequest := params.Body
	mAdmin, err := NewMinioAdminClient(session)
	if err != nil {
		return ErrorWithContext(ctx, err)
	}
	// create a MinIO Admin Client interface implementation
	// defining the client to be used
	adminClient := AdminClient{Client: mAdmin}

	groupList, _ := adminClient.listGroups(ctx)

	for _, b := range groupList {
		if b == *groupRequest.Group {
			return ErrorWithContext(ctx, ErrGroupAlreadyExists)
		}
	}

	if err := addGroup(ctx, adminClient, *groupRequest.Group, groupRequest.Members); err != nil {
		return ErrorWithContext(ctx, err)
	}
	return nil
}

// removeGroup deletes a minIO group only if it has no members
func removeGroup(ctx context.Context, client MinioAdmin, group string) error {
	gAddRemove := madmin.GroupAddRemove{
		Group:    group,
		Members:  []string{},
		IsRemove: true,
	}
	err := client.updateGroupMembers(ctx, gAddRemove)
	if err != nil {
		return err
	}
	return nil
}

// getRemoveGroupResponse performs removeGroup() and serializes it to the handler's output
func getRemoveGroupResponse(session *models.Principal, params groupApi.RemoveGroupParams) *models.Error {
	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
	defer cancel()
	if params.Name == "" {
		return ErrorWithContext(ctx, ErrGroupNameNotInRequest)
	}
	mAdmin, err := NewMinioAdminClient(session)
	if err != nil {
		return ErrorWithContext(ctx, err)
	}
	// Create a MinIO Admin Client interface implementation
	// defining the client to be used
	adminClient := AdminClient{Client: mAdmin}

	groupName, err := utils.DecodeBase64(params.Name)
	if err != nil {
		return ErrorWithContext(ctx, err)
	}

	if err := removeGroup(ctx, adminClient, groupName); err != nil {
		minioError := madmin.ToErrorResponse(err)
		err2 := ErrorWithContext(ctx, err)
		if minioError.Code == "XMinioAdminNoSuchGroup" {
			err2.Code = 404
		}
		return err2
	}
	return nil
}

// updateGroup updates a group by adding/removing members and setting the status to the desired one
//
// isRemove: whether remove members or not
func updateGroupMembers(ctx context.Context, client MinioAdmin, group string, members []string, isRemove bool) error {
	gAddRemove := madmin.GroupAddRemove{
		Group:    group,
		Members:  members,
		IsRemove: isRemove,
	}
	err := client.updateGroupMembers(ctx, gAddRemove)
	if err != nil {
		return err
	}
	return nil
}

// addOrDeleteMembers updates a group members by adding or deleting them based on the expectedMembers
func addOrDeleteMembers(ctx context.Context, client MinioAdmin, group *madmin.GroupDesc, expectedMembers []string) error {
	// get members to delete/add
	membersToDelete := DifferenceArrays(group.Members, expectedMembers)
	membersToAdd := DifferenceArrays(expectedMembers, group.Members)
	// delete members if any to be deleted
	if len(membersToDelete) > 0 {
		err := updateGroupMembers(ctx, client, group.Name, membersToDelete, true)
		if err != nil {
			return err
		}
	}
	// add members if any to be added
	if len(membersToAdd) > 0 {
		err := updateGroupMembers(ctx, client, group.Name, membersToAdd, false)
		if err != nil {
			return err
		}
	}
	return nil
}

func setGroupStatus(ctx context.Context, client MinioAdmin, group, status string) error {
	var setStatus madmin.GroupStatus
	switch status {
	case "enabled":
		setStatus = madmin.GroupEnabled
	case "disabled":
		setStatus = madmin.GroupDisabled
	default:
		return errors.New(500, "status not valid")
	}
	return client.setGroupStatus(ctx, group, setStatus)
}

// getUpdateGroupResponse updates a group by adding or removing it's members depending on the request,
// also sets the group's status if status in the request is different than the current one.
// Then serializes the output to be used by the handler.
func getUpdateGroupResponse(session *models.Principal, params groupApi.UpdateGroupParams) (*models.Group, *models.Error) {
	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
	defer cancel()
	if params.Name == "" {
		return nil, ErrorWithContext(ctx, ErrGroupNameNotInRequest)
	}
	if params.Body == nil {
		return nil, ErrorWithContext(ctx, ErrGroupBodyNotInRequest)
	}
	expectedGroupUpdate := params.Body

	groupName, err := utils.DecodeBase64(params.Name)
	if err != nil {
		return nil, ErrorWithContext(ctx, err)
	}

	mAdmin, err := NewMinioAdminClient(session)
	if err != nil {
		return nil, ErrorWithContext(ctx, err)
	}
	// create a MinIO Admin Client interface implementation
	// defining the client to be used
	adminClient := AdminClient{Client: mAdmin}

	groupUpdated, err := groupUpdate(ctx, adminClient, groupName, expectedGroupUpdate)
	if err != nil {
		return nil, ErrorWithContext(ctx, err)
	}
	groupResponse := &models.Group{
		Name:    groupUpdated.Name,
		Members: groupUpdated.Members,
		Policy:  groupUpdated.Policy,
		Status:  groupUpdated.Status,
	}
	return groupResponse, nil
}

// groupUpdate updates a group given the expected parameters, compares the expected parameters against the current ones
// and updates them accordingly, status is only updated if the expected status is different than the current one.
// Then fetches the group again to return the object updated.
func groupUpdate(ctx context.Context, client MinioAdmin, groupName string, expectedGroup *models.UpdateGroupRequest) (*madmin.GroupDesc, error) {
	expectedMembers := expectedGroup.Members
	expectedStatus := *expectedGroup.Status
	// get current members and status
	groupDescription, err := groupInfo(ctx, client, groupName)
	if err != nil {
		LogInfo("error getting group info: %v", err)
		return nil, err
	}
	// update group members
	err = addOrDeleteMembers(ctx, client, groupDescription, expectedMembers)
	if err != nil {
		LogInfo("error updating group: %v", err)
		return nil, err
	}
	// update group status only if different from current status
	if expectedStatus != groupDescription.Status {
		err = setGroupStatus(ctx, client, groupDescription.Name, expectedStatus)
		if err != nil {
			LogInfo("error updating group's status: %v", err)
			return nil, err
		}
	}
	// return latest group info to verify that changes were applied correctly
	groupDescription, err = groupInfo(ctx, client, groupName)
	if err != nil {
		LogInfo("error getting group info: %v", err)
		return nil, err
	}
	return groupDescription, nil
}
