// Copyright 2019 Yunion
//
// Licensed 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 db

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"time"

	"yunion.io/x/jsonutils"
	"yunion.io/x/log"
	"yunion.io/x/pkg/errors"
	"yunion.io/x/pkg/util/printutils"
	"yunion.io/x/pkg/util/rbacscope"
	"yunion.io/x/pkg/util/stringutils"
	"yunion.io/x/pkg/utils"
	"yunion.io/x/sqlchemy"

	"yunion.io/x/onecloud/pkg/apis"
	dbapi "yunion.io/x/onecloud/pkg/apis/cloudcommon/db"
	"yunion.io/x/onecloud/pkg/cloudcommon/consts"
	"yunion.io/x/onecloud/pkg/cloudcommon/db/lockman"
	"yunion.io/x/onecloud/pkg/cloudcommon/policy"
	"yunion.io/x/onecloud/pkg/httperrors"
	"yunion.io/x/onecloud/pkg/mcclient"
)

const (
	SYSTEM_ADMIN_PREFIX  = "__sys_"
	SYS_TAG_PREFIX       = "__"
	CLOUD_TAG_PREFIX     = dbapi.CLOUD_TAG_PREFIX
	USER_TAG_PREFIX      = dbapi.USER_TAG_PREFIX
	SYS_CLOUD_TAG_PREFIX = dbapi.SYS_CLOUD_TAG_PREFIX
	CLASS_TAG_PREFIX     = dbapi.CLASS_TAG_PREFIX
	SKU_METADAT_KEY      = "md5"

	ORGANIZATION_TAG_PREFIX = dbapi.ORGANIZATION_TAG_PREFIX

	// TAG_DELETE_RANGE_USER  = "user"
	// TAG_DELETE_RANGE_CLOUD = CLOUD_TAG_PREFIX // "cloud"

	TAG_DELETE_RANGE_ALL = "all"

	OBJECT_TYPE_ID_SEP = "::"

	RE_BILLING_AT = "__re_billing_at"
)

type SMetadataManager struct {
	SModelBaseManager
}

type SMetadata struct {
	SModelBase

	// 资源类型
	// example: network
	ObjType string `width:"40" charset:"ascii" index:"true" list:"user" get:"user"`

	// 资源ID
	// example: 87321a70-1ecb-422a-8b0c-c9aa632a46a7
	ObjId string `width:"88" charset:"ascii" index:"true" list:"user" get:"user"`

	// 资源组合ID
	// example: network::87321a70-1ecb-422a-8b0c-c9aa632a46a7
	Id string `width:"128" charset:"ascii" primary:"true" list:"user" get:"user"`

	// 标签KEY
	// exmaple: 部门
	Key string `width:"64" charset:"utf8" primary:"true" list:"user" get:"user"`

	// 标签值
	// example: 技术部
	Value string `charset:"utf8" list:"user" get:"user"`

	// 更新时间
	UpdatedAt time.Time `nullable:"false" updated_at:"true"`

	// 是否被删除
	Deleted bool `nullable:"false" default:"false" index:"true"`
}

var Metadata *SMetadataManager

func init() {
	Metadata = &SMetadataManager{
		SModelBaseManager: NewModelBaseManager(
			SMetadata{},
			"metadata_tbl",
			"metadata",
			"metadatas",
		),
	}
	Metadata.SetVirtualObject(Metadata)
	Metadata.TableSpec().AddIndex(false, "obj_type", "obj_id", "key", "deleted")
}

func (manager *SMetadataManager) InitializeData() error {
	/*no need to do this initilization any more
	q := manager.RawQuery()
	q = q.Filter(sqlchemy.OR(
		sqlchemy.IsNullOrEmpty(q.Field("obj_type")),
		sqlchemy.IsNullOrEmpty(q.Field("obj_id")),
	))
	mds := make([]SMetadata, 0)
	err := FetchModelObjects(manager, q, &mds)
	if err != nil && err != sql.ErrNoRows {
		return errors.Wrap(err, "FetchModelObjects")
	}
	for i := range mds {
		_, err := Update(&mds[i], func() error {
			parts := strings.Split(mds[i].Id, OBJECT_TYPE_ID_SEP)
			if len(parts) == 2 {
				mds[i].ObjType = parts[0]
				mds[i].ObjId = parts[1]
				return nil
			} else {
				return errors.Wrapf(httperrors.ErrInvalidFormat, "invlid id format %s", mds[i].Id)
			}
		})
		if err != nil {
			return errors.Wrap(err, "update")
		}
	}*/
	return nil
}

func (m *SMetadata) GetId() string {
	return fmt.Sprintf("%s-%s", m.Id, m.Key)
}

func (m *SMetadata) GetName() string {
	return fmt.Sprintf("%s-%s", m.Id, m.Key)
}

// func (m *SMetadata) GetModelManager() IModelManager {
//	return Metadata
// }

func GetModelIdstr(model IModel) string {
	return getObjectIdstr(model.GetModelManager().Keyword(), model.GetId())
}

func getObjectIdstr(objType, objId string) string {
	return fmt.Sprintf("%s%s%s", objType, OBJECT_TYPE_ID_SEP, objId)
}

func (manager *SMetadataManager) Query(fields ...string) *sqlchemy.SQuery {
	return manager.SModelBaseManager.Query(fields...).IsFalse("deleted")
}

func (manager *SMetadataManager) RawQuery(fields ...string) *sqlchemy.SQuery {
	return manager.SModelBaseManager.Query(fields...)
}

func (m *SMetadata) MarkDelete() error {
	m.Deleted = true
	return nil
}

func (m *SMetadata) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
	return DeleteModel(ctx, userCred, m)
}

func (manager *SMetadataManager) fetchKeyValueQuery(
	ctx context.Context,
	userCred mcclient.TokenCredential,
	input apis.MetaGetPropertyTagValuePairsInput,
) (*sqlchemy.SQuery, error) {
	var err error
	sq := manager.Query().SubQuery()
	keyOnly := (input.KeyOnly != nil && *input.KeyOnly)
	var q *sqlchemy.SQuery
	var queryFields []sqlchemy.IQueryField
	if keyOnly {
		queryFields = []sqlchemy.IQueryField{
			sq.Field("key"),
			sqlchemy.COUNT("count", sq.Field("key")),
		}
	} else {
		queryFields = []sqlchemy.IQueryField{
			sq.Field("key"),
			sq.Field("value"),
			sqlchemy.COUNT("count", sq.Field("key")),
		}
	}
	q = sq.Query(queryFields...)

	q, err = manager.ListItemFilter(ctx, q, userCred, input.MetadataListInput)
	if err != nil {
		return nil, errors.Wrap(err, "ListItemFilter")
	}

	if keyOnly {
		q = q.GroupBy(q.Field("key"))
	} else {
		q = q.GroupBy(q.Field("key"), q.Field("value"))
	}
	if input.Order == string(sqlchemy.SQL_ORDER_DESC) {
		q = q.Desc(q.Field("key"))
		if !keyOnly {
			q = q.Desc(q.Field("value"))
		}
	} else {
		q = q.Asc(q.Field("key"))
		if !keyOnly {
			q = q.Asc(q.Field("value"))
		}
	}

	return q, nil
}

// +onecloud:swagger-gen-ignore
func (manager *SMetadataManager) GetPropertyTagValuePairs(
	ctx context.Context,
	userCred mcclient.TokenCredential,
	input apis.MetaGetPropertyTagValuePairsInput,
) (*printutils.ListResult, error) {
	q, err := manager.fetchKeyValueQuery(ctx, userCred, input)
	if err != nil {
		return nil, errors.Wrap(err, "fetchKeyValueQuery")
	}

	totalCnt, err := q.CountWithError()
	if err != nil {
		return nil, errors.Wrap(err, "CountWithError")
	}

	if totalCnt == 0 {
		emptyList := printutils.ListResult{Data: []jsonutils.JSONObject{}}
		return &emptyList, nil
	}

	maxLimit := consts.GetMaxPagingLimit()
	limit := consts.GetDefaultPagingLimit()
	if input.Limit != nil {
		limit = int64(*input.Limit)
	}
	offset := int64(0)
	if input.Offset != nil {
		offset = int64(*input.Offset)
	}
	if offset < 0 {
		offset = int64(totalCnt) + offset
	}
	if offset < 0 {
		offset = 0
	}
	if int64(totalCnt) > maxLimit && (limit <= 0 || limit > maxLimit) {
		limit = maxLimit
	}
	if limit > 0 {
		q = q.Limit(int(limit))
	}
	if offset > 0 {
		q = q.Offset(int(offset))
	}

	data, err := manager.metaDataQuery2List(ctx, q, userCred, input.MetadataListInput)
	if err != nil {
		return nil, errors.Wrap(err, "metadataQuery2List")
	}
	emptyList := printutils.ListResult{
		Data:   data,
		Total:  totalCnt,
		Limit:  int(limit),
		Offset: int(offset),
	}
	return &emptyList, nil
}

func (manager *SMetadataManager) metaDataQuery2List(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input apis.MetadataListInput) ([]jsonutils.JSONObject, error) {
	metadatas := make([]struct {
		Key   string
		Value string
		Count int64
	}, 0)
	err := q.All(&metadatas)
	if err != nil {
		return nil, errors.Wrap(err, "Query.All")
	}

	ret := make([]jsonutils.JSONObject, len(metadatas))
	keys := []string{}
	for i := range metadatas {
		if !utils.IsInStringArray(metadatas[i].Key, keys) {
			keys = append(keys, metadatas[i].Key)
		}
		ret[i] = jsonutils.Marshal(metadatas[i])
	}

	if input.Details == nil || !*input.Details {
		return ret, nil
	}
	mQ, err := manager.ListItemFilter(ctx, manager.Query(), userCred, input)
	if err != nil {
		return nil, errors.Wrap(err, "ListItemFilter")
	}
	metas := []SMetadata{}
	err = mQ.In("key", keys).All(&metas)
	if err != nil {
		return ret, errors.Wrapf(err, "q.All")
	}
	count := map[string]map[string]map[string]int64{}
	for i := range metas {
		meta := metas[i]
		_, ok := count[meta.Key]
		if !ok {
			count[meta.Key] = map[string]map[string]int64{}
		}
		_, ok = count[meta.Key][meta.Value]
		if !ok {
			count[meta.Key][meta.Value] = map[string]int64{}
		}
		k := fmt.Sprintf("%s_count", meta.ObjType)
		_, ok = count[meta.Key][meta.Value][k]
		if !ok {
			count[meta.Key][meta.Value][k] = 0
		}
		count[meta.Key][meta.Value][k] += 1
	}
	for i, meta := range metadatas {
		jsonutils.Update(ret[i], count[meta.Key][meta.Value])
	}

	return ret, nil
}

func (manager *SMetadataManager) metadataBaseFilter(q *sqlchemy.SQuery, input apis.MetadataBaseFilterInput) *sqlchemy.SQuery {
	if len(input.KeyLike) > 0 {
		q = q.Contains("key", input.KeyLike)
	}
	if len(input.ValueLike) > 0 {
		q = q.Contains("value", input.ValueLike)
	}
	if len(input.Key) > 0 {
		q = q.In("key", input.Key)
	}
	if len(input.Value) > 0 {
		q = q.In("value", input.Value)
	}
	if input.SysMeta != nil && *input.SysMeta {
		q = q.Filter(sqlchemy.Startswith(q.Field("key"), SYS_TAG_PREFIX))
	}
	if input.CloudMeta != nil && *input.CloudMeta {
		q = q.Filter(sqlchemy.Startswith(q.Field("key"), CLOUD_TAG_PREFIX))
	}
	if input.UserMeta != nil && *input.UserMeta {
		q = q.Filter(sqlchemy.Startswith(q.Field("key"), USER_TAG_PREFIX))
	}
	withConditions := []sqlchemy.ICondition{}
	if input.WithSysMeta != nil && *input.WithSysMeta {
		withConditions = append(withConditions, sqlchemy.Startswith(q.Field("key"), SYS_TAG_PREFIX))
	}
	if input.WithCloudMeta != nil && *input.WithCloudMeta {
		withConditions = append(withConditions, sqlchemy.Startswith(q.Field("key"), CLOUD_TAG_PREFIX))
	}
	if input.WithUserMeta != nil && *input.WithUserMeta {
		withConditions = append(withConditions, sqlchemy.Startswith(q.Field("key"), USER_TAG_PREFIX))
	}
	if len(withConditions) > 0 {
		q = q.Filter(sqlchemy.OR(withConditions...))
	}
	return q
}

// 元数据(标签)列表
func (manager *SMetadataManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input apis.MetadataListInput) (*sqlchemy.SQuery, error) {
	var err error
	q, err = manager.SModelBaseManager.ListItemFilter(ctx, q, userCred, input.ModelBaseListInput)
	if err != nil {
		return nil, errors.Wrap(err, "SModelBaseManager.ListItemFilter")
	}

	q = manager.metadataBaseFilter(q, input.MetadataBaseFilterInput)

	if len(input.Search) > 0 {
		q = q.Filter(sqlchemy.OR(
			sqlchemy.Contains(q.Field("key"), input.Search),
			sqlchemy.Contains(q.Field("value"), input.Search),
		))
	}

	if !(input.Scope == string(rbacscope.ScopeSystem) && userCred.HasSystemAdminPrivilege()) {
		resources := input.Resources
		if len(resources) == 0 {
			for resource := range globalTables {
				resources = append(resources, resource)
			}
		}
		conditions := []sqlchemy.ICondition{}
		for _, resource := range resources {
			man, ok := globalTables[resource]
			if !ok {
				return nil, httperrors.NewNotFoundError("Not support resource %s tag filter", resource)
			}
			if !man.IsStandaloneManager() {
				continue
			}
			sq := man.Query("id")
			query := jsonutils.Marshal(input)
			ownerId, queryScope, err, _ := FetchCheckQueryOwnerScope(ctx, userCred, query, man, policy.PolicyActionList, true)
			if err != nil {
				log.Warningf("FetchCheckQueryOwnerScope.%s error: %v", man.Keyword(), err)
				continue
			}
			sq = man.FilterByOwner(ctx, sq, man, userCred, ownerId, queryScope)
			sq = man.FilterBySystemAttributes(sq, userCred, query, queryScope)
			sq = man.FilterByHiddenSystemAttributes(sq, userCred, query, queryScope)
			conditions = append(conditions, sqlchemy.In(q.Field("obj_id"), sq))
		}
		if len(conditions) > 0 {
			q = q.Filter(sqlchemy.OR(conditions...))
		}
	}

	/*for args, prefix := range map[string]string{"sys_meta": SYS_TAG_PREFIX, "cloud_meta": CLOUD_TAG_PREFIX, "user_meta": USER_TAG_PREFIX} {
		if jsonutils.QueryBoolean(query, args, false) {
			q = q.Filter(sqlchemy.Startswith(q.Field("key"), prefix))
		}
	}*/

	/*for args, prefix := range map[string]string{"with_sys_meta": SYS_TAG_PREFIX, "with_cloud_meta": CLOUD_TAG_PREFIX, "with_user_meta": USER_TAG_PREFIX} {
		if jsonutils.QueryBoolean(query, args, false) {
			withConditions = append(withConditions, sqlchemy.Startswith(q.Field("key"), prefix))
		}
	}*/

	return q, nil
}

func (manager *SMetadataManager) GetStringValue(ctx context.Context, model IModel, key string, userCred mcclient.TokenCredential) string {
	if !isAllowGetMetadata(ctx, model, userCred) {
		return ""
	}
	if strings.HasPrefix(key, SYSTEM_ADMIN_PREFIX) && (userCred == nil || !IsAllowGetSpec(ctx, rbacscope.ScopeSystem, userCred, model, "metadata")) {
		return ""
	}
	idStr := GetModelIdstr(model)
	m := SMetadata{}
	err := manager.Query("value").Equals("id", idStr).Equals("key", key).First(&m)
	if err == nil {
		return m.Value
	}
	return ""
}

func (manager *SMetadataManager) GetJsonValue(ctx context.Context, model IModel, key string, userCred mcclient.TokenCredential) jsonutils.JSONObject {
	if !isAllowGetMetadata(ctx, model, userCred) {
		return nil
	}
	if strings.HasPrefix(key, SYSTEM_ADMIN_PREFIX) && (userCred == nil || !IsAllowGetSpec(ctx, rbacscope.ScopeSystem, userCred, model, "metadata")) {
		return nil
	}
	idStr := GetModelIdstr(model)
	m := SMetadata{}
	err := manager.Query("value").Equals("id", idStr).Equals("key", key).First(&m)
	if err == nil {
		json, _ := jsonutils.ParseString(m.Value)
		return json
	}
	return nil
}

type sMetadataChange struct {
	Key    string
	OValue string
	NValue string `json:",allowempty"`
}

func (manager *SMetadataManager) RemoveAll(ctx context.Context, model IModel, userCred mcclient.TokenCredential) error {
	idStr := GetModelIdstr(model)
	if len(idStr) == 0 {
		return fmt.Errorf("invalid model")
	}

	lockman.LockObject(ctx, model)
	defer lockman.ReleaseObject(ctx, model)

	changes := []sMetadataChange{}
	records := make([]SMetadata, 0)
	q := manager.Query().Equals("id", idStr)
	err := FetchModelObjects(manager, q, &records)
	if err != nil {
		return fmt.Errorf("find metadata for %s fail: %s", idStr, err)
	}
	for _, rec := range records {
		if err = rec.Delete(ctx, userCred); err != nil {
			log.Errorf("remove metadata %v error: %v", rec, err)
			continue
		}
		changes = append(changes, sMetadataChange{Key: rec.Key, OValue: rec.Value})
	}
	if len(changes) > 0 {
		OpsLog.LogEvent(model, ACT_SET_METADATA, jsonutils.Marshal(changes), userCred)
	}
	return nil
}

func infMap2StrMap(input map[string]interface{}) map[string]string {
	output := make(map[string]string)
	for k, v := range input {
		output[k] = stringutils.Interface2String(v)
	}
	return output
}

func (manager *SMetadataManager) SetValue(ctx context.Context, obj IModel, key string, value interface{}, userCred mcclient.TokenCredential) error {
	return manager.SetValuesWithLog(ctx, obj, map[string]interface{}{key: value}, userCred)
}

func (manager *SMetadataManager) SetValuesWithLog(ctx context.Context, obj IModel, store map[string]interface{}, userCred mcclient.TokenCredential) error {
	lockman.LockObject(ctx, obj)
	defer lockman.ReleaseObject(ctx, obj)

	changes, err := manager.rawSetValues(ctx, obj.Keyword(), obj.GetId(), infMap2StrMap(store), false, "")
	if err != nil {
		return err
	}
	if len(changes) > 0 {
		OpsLog.LogEvent(obj.GetIModel(), ACT_SET_METADATA, jsonutils.Marshal(changes), userCred)
		for _, change := range changes {
			if change.Key == RE_BILLING_AT {
				desc := obj.GetIModel().GetShortDesc(ctx)
				desc.Set("created_at", jsonutils.Marshal(change.NValue))
				OpsLog.LogEvent(obj.GetIModel(), ACT_RE_BILLING, desc, userCred)
				break
			}
		}
	}
	return nil
}

func (manager *SMetadataManager) rawSetValues(ctx context.Context, objType string, objId string, store map[string]string, replace bool, replaceRange string) ([]sMetadataChange, error) {
	idStr := getObjectIdstr(objType, objId)

	keys := make([]string, 0, len(store))
	changes := make([]sMetadataChange, 0)
	for key, value := range store {
		keys = append(keys, key)

		record := SMetadata{}
		record.SetModelManager(manager, &record)

		err := manager.RawQuery().Equals("id", idStr).Equals("key", key).First(&record) //避免之前设置的tag被删除后再次设置时出现Duplicate entry error
		if err != nil {
			if errors.Cause(err) != sql.ErrNoRows {
				return changes, errors.Wrap(err, "RawQuery")
			} else {
				record.Deleted = true
			}
		}

		newRecord := SMetadata{}

		valStr := value
		valStrLower := strings.ToLower(valStr)
		if valStrLower == "none" || valStrLower == "null" {
			newRecord.Value = record.Value
			newRecord.Deleted = true
		} else {
			newRecord.Value = valStr
			newRecord.Deleted = false
		}

		if record.Deleted == newRecord.Deleted && record.Value == newRecord.Value {
			// no changes
			continue
		}

		newRecord.SetModelManager(manager, &newRecord)

		newRecord.ObjId = objId
		newRecord.ObjType = objType
		newRecord.Id = idStr
		newRecord.Key = key

		if len(record.Id) == 0 {
			err = manager.TableSpec().InsertOrUpdate(ctx, &newRecord)
		} else {
			rV, rD := record.Value, record.Deleted
			_, err = Update(&record, func() error {
				record.Value = newRecord.Value
				record.Key = key
				record.Deleted = newRecord.Deleted
				return nil
			})
			record.Value, record.Deleted = rV, rD
		}
		if err != nil {
			return nil, errors.Wrapf(err, "InsertOrUpdate %s=%s", key, valStr)
		}

		if record.Deleted != newRecord.Deleted {
			if record.Deleted {
				// create
				changes = append(changes, sMetadataChange{Key: key, NValue: valStr})
			} else {
				// delete
				changes = append(changes, sMetadataChange{Key: key, OValue: record.Value})
			}
		} else {
			// change
			changes = append(changes, sMetadataChange{Key: key, OValue: record.Value, NValue: valStr})
		}
	}
	if replace {
		records := []SMetadata{}
		q := manager.Query().Equals("id", idStr).NotLike("key", `\_\_%`) //避免删除系统内置的metadata, _ 在mysql里面有特殊含义,需要转义
		// switch replaceRange {
		// case USER_TAG_PREFIX:
		// 	q = q.Startswith("key", USER_TAG_PREFIX)
		// case CLOUD_TAG_PREFIX:
		// 	q = q.Startswith("key", CLOUD_TAG_PREFIX)
		// case SYS_CLOUD_TAG_PREFIX:
		// 	q = q.Startswith("key", SYS_CLOUD_TAG_PREFIX)
		// }
		q = q.Startswith("key", replaceRange)
		q = q.Filter(sqlchemy.NOT(sqlchemy.In(q.Field("key"), keys)))
		if err := FetchModelObjects(manager, q, &records); err != nil {
			log.Errorf("failed to fetch metadata error: %v", err)
		}
		for _, rec := range records {
			_, err := Update(&rec, func() error {
				rec.Deleted = true
				return nil
			})
			if err != nil {
				log.Errorf("failed to delete metadata record %s %s %s", objType, objId, rec.Key)
			} else {
				changes = append(changes, sMetadataChange{Key: rec.Key, OValue: rec.Value})
			}
		}
	}
	return changes, nil
}

func (manager *SMetadataManager) SetAllWithoutDelelte(ctx context.Context, obj IModel, store map[string]interface{}, userCred mcclient.TokenCredential) error {
	lockman.LockObject(ctx, obj)
	defer lockman.ReleaseObject(ctx, obj)

	changes, err := manager.rawSetValues(ctx, obj.Keyword(), obj.GetId(), infMap2StrMap(store), false, "")
	if err != nil {
		return errors.Wrap(err, "setValues")
	}

	if len(changes) > 0 {
		OpsLog.LogEvent(obj.GetIModel(), ACT_SET_METADATA, jsonutils.Marshal(changes), userCred)
	}
	return nil
}

func (manager *SMetadataManager) SetAll(ctx context.Context, obj IModel, store map[string]interface{}, userCred mcclient.TokenCredential, delRange string) error {
	lockman.LockObject(ctx, obj)
	defer lockman.ReleaseObject(ctx, obj)

	changes, err := manager.rawSetValues(ctx, obj.Keyword(), obj.GetId(), infMap2StrMap(store), true, delRange)
	if err != nil {
		return errors.Wrap(err, "setValues")
	}

	if len(changes) > 0 {
		OpsLog.LogEvent(obj.GetIModel(), ACT_SET_METADATA, jsonutils.Marshal(changes), userCred)
	}
	return nil
}

func isAllowGetMetadata(ctx context.Context, obj IModel, userCred mcclient.TokenCredential) bool {
	if userCred != nil {
		for _, scope := range []rbacscope.TRbacScope{
			rbacscope.ScopeSystem,
			rbacscope.ScopeDomain,
			rbacscope.ScopeProject,
		} {
			if IsAllowGetSpec(ctx, scope, userCred, obj, "metadata") {
				return true
			}
		}
		return false
	}
	return true
}

type IMetadataGetter interface {
	GetId() string
	Keyword() string
}

func (manager *SMetadataManager) GetAll(ctx context.Context, obj IMetadataGetter, keys []string, keyPrefix string, userCred mcclient.TokenCredential) (map[string]string, error) {
	modelObj, isIModel := obj.(IModel)

	if isIModel && !isAllowGetMetadata(ctx, modelObj, userCred) {
		return map[string]string{}, nil
	}
	meta, err := manager.rawGetAll(obj.Keyword(), obj.GetId(), keys, keyPrefix)
	if err != nil {
		return nil, errors.Wrap(err, "rawGetAll")
	}
	ret := make(map[string]string)
	for k, v := range meta {
		if strings.HasPrefix(k, SYSTEM_ADMIN_PREFIX) && (userCred == nil || (isIModel && !IsAllowGetSpec(ctx, rbacscope.ScopeSystem, userCred, modelObj, "metadata"))) {
			continue
		}
		ret[k] = v
	}
	return ret, nil
}

func (manager *SMetadataManager) rawGetAll(objType, objId string, keys []string, keyPrefix string) (map[string]string, error) {
	idStr := getObjectIdstr(objType, objId)
	records := make([]SMetadata, 0)
	q := manager.Query().Equals("id", idStr)
	if len(keys) > 0 {
		q = q.In("key", keys)
	}
	if len(keyPrefix) > 0 {
		q = q.Startswith("key", keyPrefix)
	}
	err := FetchModelObjects(manager, q, &records)
	if err != nil {
		return nil, errors.Wrap(err, "FetchModelObjects")
	}
	ret := make(map[string]string)
	for _, rec := range records {
		if len(rec.Value) > 0 || strings.HasPrefix(rec.Key, USER_TAG_PREFIX) || strings.HasPrefix(rec.Key, CLOUD_TAG_PREFIX) {
			ret[rec.Key] = rec.Value
		}
	}
	return ret, nil
}

/*func (manager *SMetadataManager) IsSystemAdminKey(key string) bool {
	return isMetadataKeySystemAdmin(key)
}*/

func isMetadataLoginKey(key string) bool {
	return strings.HasPrefix(key, "login_")
}

func isMetadataKeySystemAdmin(key string) bool {
	return strings.HasPrefix(key, SYSTEM_ADMIN_PREFIX)
}

func isMetadataKeyPrivateKey(key string) bool {
	for _, k := range []string{"admin", "project"} {
		for _, v := range []string{"ssh-private-key", "ssh-public-key"} {
			if key == fmt.Sprintf("%s-%s", k, v) {
				return true
			}
		}
	}
	return strings.HasPrefix(key, SYSTEM_ADMIN_PREFIX)
}

func isMetadataKeySysTag(key string) bool {
	return strings.HasPrefix(key, SYS_TAG_PREFIX)
}

func (manager *SMetadataManager) GetSysadminKey(key string) string {
	return fmt.Sprintf("%s%s", SYSTEM_ADMIN_PREFIX, key)
}

func IsMetadataKeyVisible(key string) bool {
	return !(isMetadataKeySysTag(key) || isMetadataKeySystemAdmin(key) || isMetadataKeyPrivateKey(key))
}

func GetVisibleMetadata(ctx context.Context, model IStandaloneModel, userCred mcclient.TokenCredential) (map[string]string, error) {
	metaData, err := model.GetAllMetadata(ctx, userCred)
	if err != nil {
		return nil, err
	}
	for _, key := range model.StandaloneModelManager().GetMetadataHiddenKeys() {
		delete(metaData, key)
	}
	for key := range metaData {
		if !IsMetadataKeyVisible(key) {
			delete(metaData, key)
		}
	}
	return metaData, nil
}

func metaList2Map(manager IMetadataBaseModelManager, userCred mcclient.TokenCredential, metaList []SMetadata) map[string]string {
	metaMap := make(map[string]string)

	hiddenKeys := manager.GetMetadataHiddenKeys()
	for _, meta := range metaList {
		if IsMetadataKeyVisible(meta.Key) && !utils.IsInStringArray(meta.Key, hiddenKeys) {
			metaMap[meta.Key] = meta.Value
		}
	}

	return metaMap
}

func CopyTags(ctx context.Context, objType string, keys1 []string, values []string, keys2 []string) error {
	return Metadata.copyTags(ctx, objType, keys1, values, keys2)
}

func (manager *SMetadataManager) copyTags(ctx context.Context, objType string, keys1 []string, values []string, keys2 []string) error {
	for i := 0; i < len(keys1) && i < len(values) && i < len(keys2); i++ {
		key1 := keys1[i]
		key2 := keys2[i]
		value := values[i]

		q := manager.Query("obj_id").Equals("obj_type", objType).Equals("key", key1).Equals("value", value)
		q2 := manager.Query("obj_id").Equals("obj_type", objType).Equals("key", key2).Equals("value", value)
		q = q.NotIn("obj_id", q2.SubQuery())

		results := []struct {
			ObjId string
		}{}
		err := q.All(&results)
		if err != nil {
			return errors.Wrapf(err, "copy key %s value %s to %s", key1, value, key2)
		}

		for _, result := range results {
			record := SMetadata{}
			record.SetModelManager(manager, &record)

			record.ObjId = result.ObjId
			record.ObjType = objType
			record.Id = getObjectIdstr(objType, result.ObjId)
			record.Key = key2
			record.Value = value

			err := manager.TableSpec().InsertOrUpdate(ctx, &record)
			if err != nil {
				return errors.Wrapf(err, "insert %s", jsonutils.Marshal(record))
			}
		}
	}
	return nil
}
