/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package mongo

import (
	"context"
	"strconv"
	"time"

	dmongo "github.com/go-chassis/cari/db/mongo"
	rbacmodel "github.com/go-chassis/cari/rbac"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"

	"github.com/apache/servicecomb-service-center/datasource"
	"github.com/apache/servicecomb-service-center/datasource/mongo/dao"
	"github.com/apache/servicecomb-service-center/datasource/mongo/model"
	"github.com/apache/servicecomb-service-center/datasource/mongo/sync"
	mutil "github.com/apache/servicecomb-service-center/datasource/mongo/util"
	"github.com/apache/servicecomb-service-center/datasource/rbac"
	"github.com/apache/servicecomb-service-center/pkg/log"
	"github.com/apache/servicecomb-service-center/pkg/util"
)

func (ds *RbacDAO) CreateRole(ctx context.Context, r *rbacmodel.Role) error {
	exist, err := ds.RoleExist(ctx, r.Name)
	if err != nil {
		log.Error("failed to query role", err)
		return err
	}
	if exist {
		return rbac.ErrRoleDuplicated
	}
	r.ID = util.GenerateUUID()
	r.CreateTime = strconv.FormatInt(time.Now().Unix(), 10)
	r.UpdateTime = r.CreateTime
	err = createRoleTxn(ctx, r)
	if err != nil {
		if dao.IsDuplicateKey(err) {
			return rbac.ErrRoleDuplicated
		}
		return err
	}
	log.Info("succeed to create new role: " + r.ID)
	return nil
}

func createRoleTxn(ctx context.Context, r *rbacmodel.Role) error {
	return dmongo.GetClient().ExecTxn(ctx, func(sessionContext mongo.SessionContext) error {
		_, err := dmongo.GetClient().GetDB().Collection(model.CollectionRole).InsertOne(ctx, r)
		if err != nil {
			return err
		}
		return sync.DoCreateOpts(sessionContext, datasource.ResourceRole, r)
	})
}

func (ds *RbacDAO) RoleExist(ctx context.Context, name string) (bool, error) {
	filter := mutil.NewFilter(mutil.RoleName(name))
	count, err := dmongo.GetClient().GetDB().Collection(model.CollectionRole).CountDocuments(ctx, filter)
	if err != nil {
		return false, err
	}
	if count == 0 {
		return false, nil
	}
	return true, nil
}

func (ds *RbacDAO) GetRole(ctx context.Context, name string) (*rbacmodel.Role, error) {
	filter := mutil.NewFilter(mutil.RoleName(name))
	result := dmongo.GetClient().GetDB().Collection(model.CollectionRole).FindOne(ctx, filter)
	if result.Err() != nil {
		return nil, rbac.ErrRoleNotExist
	}
	var role rbacmodel.Role
	err := result.Decode(&role)
	if err != nil {
		log.Error("failed to decode role", err)
		return nil, err
	}
	return &role, nil
}

func (ds *RbacDAO) ListRole(ctx context.Context) ([]*rbacmodel.Role, int64, error) {
	filter := mutil.NewFilter()
	cursor, err := dmongo.GetClient().GetDB().Collection(model.CollectionRole).Find(ctx, filter)
	if err != nil {
		return nil, 0, err
	}
	var roles []*rbacmodel.Role
	defer cursor.Close(ctx)
	for cursor.Next(ctx) {
		var role rbacmodel.Role
		err = cursor.Decode(&role)
		if err != nil {
			log.Error("failed to decode role", err)
			continue
		}
		roles = append(roles, &role)
	}
	return roles, int64(len(roles)), nil
}

func (ds *RbacDAO) DeleteRole(ctx context.Context, name string) (bool, error) {
	n, err := dmongo.GetClient().GetDB().Collection(model.CollectionAccount).CountDocuments(ctx,
		bson.M{"roles": bson.M{"$in": []string{name}}})
	if err != nil {
		return false, err
	}
	if n > 0 {
		return false, rbac.ErrRoleBindingExist
	}
	err = deleteRoleTxn(ctx, name)
	if err != nil {
		return false, err
	}
	return true, nil
}

func deleteRoleTxn(ctx context.Context, name string) error {
	return dmongo.GetClient().ExecTxn(ctx, func(sessionContext mongo.SessionContext) error {
		filter := mutil.NewFilter(mutil.RoleName(name))
		_, err := dmongo.GetClient().GetDB().Collection(model.CollectionRole).DeleteOne(ctx, filter)
		if err != nil {
			return err
		}
		return sync.DoDeleteOpts(sessionContext, datasource.ResourceRole, name, name)
	})
}

func (ds *RbacDAO) UpdateRole(ctx context.Context, name string, role *rbacmodel.Role) error {
	filter := mutil.NewFilter(mutil.RoleName(name))
	setFilter := mutil.NewFilter(
		mutil.ID(role.ID),
		mutil.RoleName(role.Name),
		mutil.Perms(role.Perms),
		mutil.RoleUpdateTime(strconv.FormatInt(time.Now().Unix(), 10)),
	)
	updateFilter := mutil.NewFilter(mutil.Set(setFilter))
	return updateRoleTxn(ctx, filter, updateFilter, role)
}

func updateRoleTxn(ctx context.Context, filter bson.M, updateFilter bson.M, role *rbacmodel.Role) error {
	return dmongo.GetClient().ExecTxn(ctx, func(sessionContext mongo.SessionContext) error {
		_, err := dmongo.GetClient().GetDB().Collection(model.CollectionRole).UpdateMany(ctx, filter, updateFilter)
		if err != nil {
			return err
		}
		return sync.DoUpdateOpts(sessionContext, datasource.ResourceRole, role)
	})
}

func (ds *RbacDAO) MigrateOldRoles(_ context.Context) error {
	return nil
}
