/*
 * 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 etcd

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

	crbac "github.com/go-chassis/cari/rbac"
	"github.com/go-chassis/foundation/stringutil"
	"github.com/little-cui/etcdadpt"

	"github.com/apache/servicecomb-service-center/datasource"
	"github.com/apache/servicecomb-service-center/datasource/etcd/path"
	esync "github.com/apache/servicecomb-service-center/datasource/etcd/sync"
	"github.com/apache/servicecomb-service-center/datasource/rbac"
	"github.com/apache/servicecomb-service-center/pkg/log"
)

func init() {
	rbac.Install("etcd", NewRbacDAO)
	rbac.Install("embeded_etcd", NewRbacDAO)
	rbac.Install("embedded_etcd", NewRbacDAO)
}

func NewRbacDAO(_ rbac.Options) (rbac.DAO, error) {
	return &RbacDAO{}, nil
}

type RbacDAO struct {
}

func (ds *RbacDAO) CreateAccount(ctx context.Context, a *crbac.Account) error {
	exist, err := ds.AccountExist(ctx, a.Name)
	if err != nil {
		log.Error("can not save account info", err)
		return err
	}
	if exist {
		return rbac.ErrAccountDuplicated
	}

	opts, err := GenAccountOpts(a, etcdadpt.ActionPut)
	if err != nil {
		log.Error("", err)
		return err
	}
	syncOpts, err := esync.GenCreateOpts(ctx, datasource.ResourceAccount, a)
	if err != nil {
		log.Error("fail to create sync opts", err)
		return err
	}
	opts = append(opts, syncOpts...)
	err = etcdadpt.Txn(ctx, opts)
	if err != nil {
		log.Error("can not save account info", err)
		return err
	}
	log.Info("create new account: " + a.ID)
	return nil
}

func GenAccountOpts(a *crbac.Account, action etcdadpt.Action) ([]etcdadpt.OpOptions, error) {
	opts := make([]etcdadpt.OpOptions, 0)
	value, err := json.Marshal(a)
	if err != nil {
		log.Error("account info is invalid", err)
		return nil, err
	}
	opts = append(opts, etcdadpt.OpOptions{
		Key:    stringutil.Str2bytes(path.GenerateAccountKey(a.Name)),
		Value:  value,
		Action: action,
	})
	for _, r := range a.Roles {
		opt := etcdadpt.OpOptions{
			Key:    stringutil.Str2bytes(path.GenRoleAccountIdxKey(r, a.Name)),
			Action: action,
		}
		opts = append(opts, opt)
	}

	return opts, nil
}

func (ds *RbacDAO) AccountExist(ctx context.Context, name string) (bool, error) {
	return etcdadpt.Exist(ctx, path.GenerateRBACAccountKey(name))
}
func (ds *RbacDAO) GetAccount(ctx context.Context, name string) (*crbac.Account, error) {
	kv, err := etcdadpt.Get(ctx, path.GenerateRBACAccountKey(name))
	if err != nil {
		return nil, err
	}
	if kv == nil {
		return nil, rbac.ErrAccountNotExist
	}
	account := &crbac.Account{}
	err = json.Unmarshal(kv.Value, account)
	if err != nil {
		log.Error("account info format invalid", err)
		return nil, err
	}
	ds.compatibleOldVersionAccount(account)
	return account, nil
}

func (ds *RbacDAO) compatibleOldVersionAccount(a *crbac.Account) {
	// old version use Role, now use Roles
	// Role/Roles will not exist at the same time
	if len(a.Role) == 0 {
		return
	}
	a.Roles = []string{a.Role}
	a.Role = ""
}

func (ds *RbacDAO) ListAccount(ctx context.Context) ([]*crbac.Account, int64, error) {
	kvs, n, err := etcdadpt.List(ctx, path.GenerateRBACAccountKey(""))
	if err != nil {
		return nil, 0, err
	}
	accounts := make([]*crbac.Account, 0, n)
	for _, v := range kvs {
		a := &crbac.Account{}
		err = json.Unmarshal(v.Value, a)
		if err != nil {
			log.Error("account info format invalid:", err)
			continue //do not fail if some account is invalid
		}
		a.Password = ""
		ds.compatibleOldVersionAccount(a)
		accounts = append(accounts, a)
	}
	return accounts, n, nil
}
func (ds *RbacDAO) DeleteAccount(ctx context.Context, names []string) (bool, error) {
	if len(names) == 0 {
		return false, nil
	}
	var allOpts []etcdadpt.OpOptions
	for _, name := range names {
		a, err := ds.GetAccount(ctx, name)
		if err != nil {
			log.Error("", err)
			continue //do not fail if some account is invalid
		}
		if a == nil {
			log.Warn("can not find account")
			continue
		}
		opts, err := GenAccountOpts(a, etcdadpt.ActionDelete)
		if err != nil {
			log.Error("", err)
			continue //do not fail if some account is invalid

		}
		allOpts = append(allOpts, opts...)
		syncOpts, err := esync.GenDeleteOpts(ctx, datasource.ResourceAccount, a.Name, a)
		if err != nil {
			log.Error("fail to create sync opts", err)
			return false, err
		}
		allOpts = append(allOpts, syncOpts...)
	}
	err := etcdadpt.Txn(ctx, allOpts)
	if err != nil {
		log.Error(rbac.ErrDeleteAccountFailed.Error(), err)
		return false, err
	}
	return true, nil
}

func (ds *RbacDAO) UpdateAccount(ctx context.Context, _ string, account *crbac.Account) error {
	var (
		opts []etcdadpt.OpOptions
		err  error
	)

	account.UpdateTime = strconv.FormatInt(time.Now().Unix(), 10)

	opts, err = GenAccountOpts(account, etcdadpt.ActionPut)
	if err != nil {
		log.Error("GenAccountOpts failed", err)
		return err
	}

	old, err := ds.GetAccount(ctx, account.Name)
	if err != nil {
		log.Error("GetAccount failed", err)
		return err
	}

	for _, r := range old.Roles {
		if hasRole(account, r) {
			continue
		}
		opt := etcdadpt.OpOptions{
			Key:    stringutil.Str2bytes(path.GenRoleAccountIdxKey(r, old.Name)),
			Action: etcdadpt.ActionDelete,
		}
		opts = append(opts, opt)
	}
	syncOpts, err := esync.GenUpdateOpts(ctx, datasource.ResourceAccount, account)
	if err != nil {
		log.Error("fail to create sync opts", err)
		return err
	}
	opts = append(opts, syncOpts...)
	err = etcdadpt.Txn(ctx, opts)
	if err != nil {
		log.Error("BatchCommit failed", err)
	}
	return err
}

func hasRole(account *crbac.Account, r string) bool {
	for _, n := range account.Roles {
		if r == n {
			return true
		}
	}
	return false
}
