// 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 models

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

	"yunion.io/x/cloudmux/pkg/cloudprovider"
	"yunion.io/x/jsonutils"
	"yunion.io/x/log"
	"yunion.io/x/pkg/errors"
	"yunion.io/x/pkg/util/rbacscope"
	"yunion.io/x/pkg/utils"
	"yunion.io/x/sqlchemy"

	api "yunion.io/x/onecloud/pkg/apis/compute"
	schedapi "yunion.io/x/onecloud/pkg/apis/scheduler"
	"yunion.io/x/onecloud/pkg/cloudcommon/db"
	"yunion.io/x/onecloud/pkg/cloudcommon/db/lockman"
	"yunion.io/x/onecloud/pkg/cloudcommon/db/quotas"
	"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
	"yunion.io/x/onecloud/pkg/compute/options"
	"yunion.io/x/onecloud/pkg/httperrors"
	"yunion.io/x/onecloud/pkg/mcclient"
	"yunion.io/x/onecloud/pkg/util/rbacutils"
	"yunion.io/x/onecloud/pkg/util/stringutils2"
)

func init() {
	InstanceSnapshotManager = &SInstanceSnapshotManager{
		SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager(
			SInstanceSnapshot{},
			"instance_snapshots_tbl",
			"instance_snapshot",
			"instance_snapshots",
		),
	}
	InstanceSnapshotManager.SetVirtualObject(InstanceSnapshotManager)
}

type SInstanceSnapshot struct {
	db.SVirtualResourceBase
	db.SExternalizedResourceBase

	SManagedResourceBase
	SCloudregionResourceBase
	db.SMultiArchResourceBase

	db.SEncryptedResource

	// 云主机Id
	GuestId string `width:"36" charset:"ascii" nullable:"false" list:"user" create:"required" index:"true"`
	// 云主机配置
	ServerConfig jsonutils.JSONObject `nullable:"true" list:"user"`
	// 云主机标签
	ServerMetadata jsonutils.JSONObject `nullable:"true" list:"user"`
	// 是否自动删除
	AutoDelete bool `default:"false" update:"user" list:"user"`
	// 引用次数
	RefCount int `default:"0" list:"user"`
	// 安全组
	SecGroups jsonutils.JSONObject `nullable:"true" list:"user"`
	// 秘钥Id
	KeypairId string `width:"36" charset:"ascii" nullable:"true" list:"user"`
	// 操作系统类型
	OsType string `width:"36" charset:"ascii" nullable:"true" list:"user"`
	// 套餐名称
	InstanceType string `width:"64" charset:"utf8" nullable:"true" list:"user" create:"optional"`
	// 主机快照磁盘容量和
	// SizeMb int `nullable:"false" list:"user"`
	// 镜像ID
	ImageId string `width:"36" charset:"ascii" nullable:"true" list:"user"`
	// 是否保存内存
	WithMemory bool `default:"false" get:"user" list:"user"`
	// 内存文件大小
	MemorySizeKB int `nullable:"true" get:"user" list:"user" old_name:"memory_size_mb"`
	// 内存文件所在宿主机
	MemoryFileHostId string `width:"36" charset:"ascii" nullable:"true" get:"user" list:"user"`
	// 内存文件路径
	MemoryFilePath string `width:"512" charset:"utf8" nullable:"true" get:"user" list:"user"`
	// 内存文件校验和
	MemoryFileChecksum string `width:"32" charset:"ascii" nullable:"true" get:"user" list:"user"`
}

type SInstanceSnapshotManager struct {
	db.SVirtualResourceBaseManager
	db.SExternalizedResourceBaseManager
	SManagedResourceBaseManager
	SCloudregionResourceBaseManager
	db.SMultiArchResourceBaseManager
	db.SEncryptedResourceManager
}

var InstanceSnapshotManager *SInstanceSnapshotManager

// 主机快照列表
func (manager *SInstanceSnapshotManager) ListItemFilter(
	ctx context.Context,
	q *sqlchemy.SQuery,
	userCred mcclient.TokenCredential,
	query api.InstanceSnapshotListInput,
) (*sqlchemy.SQuery, error) {
	q, err := manager.SVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, query.VirtualResourceListInput)
	if err != nil {
		return nil, errors.Wrap(err, "SVirtualResourceBaseManager.ListItemFilter")
	}

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

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

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

	guestStr := query.ServerId
	if len(guestStr) > 0 {
		guestObj, err := GuestManager.FetchByIdOrName(ctx, userCred, guestStr)
		if err != nil {
			if errors.Cause(err) == sql.ErrNoRows {
				return nil, httperrors.NewResourceNotFoundError2("guests", guestStr)
			} else {
				return nil, httperrors.NewGeneralError(err)
			}
		}
		q = q.Equals("guest_id", guestObj.GetId())
	}

	if len(query.OsType) > 0 {
		q = q.In("os_type", query.OsType)
	}

	if query.WithMemory != nil {
		if *query.WithMemory {
			q = q.IsTrue("with_memory")
		} else {
			q = q.IsFalse("with_memory")
		}
	}

	return q, nil
}

func (manager *SInstanceSnapshotManager) OrderByExtraFields(
	ctx context.Context,
	q *sqlchemy.SQuery,
	userCred mcclient.TokenCredential,
	query api.InstanceSnapshotListInput,
) (*sqlchemy.SQuery, error) {
	var err error

	q, err = manager.SVirtualResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.VirtualResourceListInput)
	if err != nil {
		return nil, errors.Wrap(err, "SVirtualResourceBaseManager.OrderByExtraFields")
	}

	q, err = manager.SManagedResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.ManagedResourceListInput)
	if err != nil {
		return nil, errors.Wrap(err, "SManagedResourceBaseManager.OrderByExtraFields")
	}

	if db.NeedOrderQuery([]string{query.OrderByDiskSnapshotCount}) {
		nQ := SnapshotManager.Query()
		nQ = nQ.AppendField(nQ.Field("disk_id"), sqlchemy.COUNT("snapshot_count"))
		nQ = nQ.GroupBy("disk_id")
		nSQ := nQ.SubQuery()

		guestdiskQ := GuestdiskManager.Query()
		guestdiskQ = guestdiskQ.LeftJoin(nSQ, sqlchemy.Equals(nSQ.Field("disk_id"), guestdiskQ.Field("disk_id")))
		guestdiskSQ := guestdiskQ.AppendField(guestdiskQ.Field("guest_id"), nSQ.Field("snapshot_count")).SubQuery()

		q = q.LeftJoin(guestdiskSQ, sqlchemy.Equals(guestdiskSQ.Field("guest_id"), q.Field("guest_id")))
		q = q.AppendField(q.QueryFields()...)
		q = q.AppendField(guestdiskSQ.Field("snapshot_count"))
		q = db.OrderByFields(q, []string{query.OrderByDiskSnapshotCount}, []sqlchemy.IQueryField{q.Field("snapshot_count")})
	}

	if db.NeedOrderQuery([]string{query.OrderByGuest}) {
		guestQ := GuestManager.Query()
		guestSQ := guestQ.AppendField(guestQ.Field("id"), guestQ.Field("name").Label("guest_name")).SubQuery()
		q = q.LeftJoin(guestSQ, sqlchemy.Equals(guestSQ.Field("id"), q.Field("guest_id")))
		q = q.AppendField(q.QueryFields()...)
		q = q.AppendField(guestSQ.Field("guest_name"))
		q = db.OrderByFields(q, []string{query.OrderByGuest}, []sqlchemy.IQueryField{q.Field("guest_name")})
	}
	return q, nil
}

func (manager *SInstanceSnapshotManager) ListItemExportKeys(ctx context.Context,
	q *sqlchemy.SQuery,
	userCred mcclient.TokenCredential,
	keys stringutils2.SSortedStrings,
) (*sqlchemy.SQuery, error) {
	var err error
	q, err = manager.SVirtualResourceBaseManager.ListItemExportKeys(ctx, q, userCred, keys)
	if err != nil {
		return nil, err
	}

	return q, nil
}

func (manager *SInstanceSnapshotManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) {
	var err error

	q, err = manager.SVirtualResourceBaseManager.QueryDistinctExtraField(q, field)
	if err == nil {
		return q, nil
	}

	q, err = manager.SManagedResourceBaseManager.QueryDistinctExtraField(q, field)
	if err == nil {
		return q, nil
	}

	return q, httperrors.ErrNotFound
}

func (manager *SInstanceSnapshotManager) QueryDistinctExtraFields(q *sqlchemy.SQuery, resource string, fields []string) (*sqlchemy.SQuery, error) {
	var err error
	q, err = manager.SManagedResourceBaseManager.QueryDistinctExtraFields(q, resource, fields)
	if err == nil {
		return q, nil
	}
	return q, httperrors.ErrNotFound
}

func (self *SInstanceSnapshot) GetGuest() (*SGuest, error) {
	if len(self.GuestId) == 0 {
		return nil, errors.ErrNotFound
	}
	guest := GuestManager.FetchGuestById(self.GuestId)
	if guest == nil {
		return nil, errors.ErrNotFound
	}
	return guest, nil
}

func (self *SInstanceSnapshot) getMoreDetails(userCred mcclient.TokenCredential, out api.InstanceSnapshotDetails) api.InstanceSnapshotDetails {
	guest := GuestManager.FetchGuestById(self.GuestId)
	if guest != nil {
		out.Guest = guest.Name
		out.GuestStatus = guest.Status
	}
	var osType string
	provider := self.GetProviderName()
	if utils.IsInStringArray(provider, ProviderHasSubSnapshot) {
		snapshots, _ := self.GetSnapshots()
		out.Snapshots = []api.SimpleSnapshot{}
		for i := 0; i < len(snapshots); i++ {
			if snapshots[i].DiskType == api.DISK_TYPE_SYS {
				osType = snapshots[i].OsType
			}
			out.Snapshots = append(out.Snapshots, api.SimpleSnapshot{
				Id:            snapshots[i].Id,
				Name:          snapshots[i].Name,
				StorageId:     snapshots[i].StorageId,
				DiskType:      snapshots[i].DiskType,
				CloudregionId: snapshots[i].CloudregionId,
				Size:          snapshots[i].Size,
				VirtualSize:   snapshots[i].VirtualSize,
				Status:        snapshots[i].Status,
				StorageType:   snapshots[i].GetStorageType(),
				EncryptKeyId:  snapshots[i].EncryptKeyId,
				CreatedAt:     snapshots[i].CreatedAt,
			})
			out.SizeMb += snapshots[i].Size
			out.VirtualSizeMb += snapshots[i].VirtualSize
			if len(snapshots[i].StorageId) > 0 && out.StorageType == "" {
				out.StorageType = snapshots[i].GetStorageType()
			}
		}
		if out.VirtualSizeMb <= 0 && guest != nil {
			out.VirtualSizeMb = guest.getDiskSize()
		}
	} else if guest != nil {
		disk, err := guest.GetSystemDisk()
		if err != nil {
			log.Errorf("unable to GetSystemDisk of guest %q", guest.GetId())
		} else {
			s, _ := disk.GetStorage()
			if s != nil {
				out.StorageType = s.StorageType
			}
		}
		out.VirtualSizeMb = guest.getDiskSize()
		out.SizeMb = out.VirtualSizeMb
	}
	if len(osType) > 0 {
		out.Properties = map[string]string{"os_type": osType}
	}
	out.SizeMb += out.MemorySizeKB / 1024
	out.Size = out.SizeMb * 1024 * 1024
	return out
}

func (manager *SInstanceSnapshotManager) FetchCustomizeColumns(
	ctx context.Context,
	userCred mcclient.TokenCredential,
	query jsonutils.JSONObject,
	objs []interface{},
	fields stringutils2.SSortedStrings,
	isList bool,
) []api.InstanceSnapshotDetails {
	rows := make([]api.InstanceSnapshotDetails, len(objs))

	virtRows := manager.SVirtualResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
	manRows := manager.SManagedResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
	encRows := manager.SEncryptedResourceManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)

	for i := range rows {
		rows[i] = api.InstanceSnapshotDetails{
			VirtualResourceDetails: virtRows[i],
			ManagedResourceInfo:    manRows[i],

			EncryptedResourceDetails: encRows[i],
		}
		rows[i] = objs[i].(*SInstanceSnapshot).getMoreDetails(userCred, rows[i])
	}

	return rows
}

func (self *SInstanceSnapshot) StartCreateInstanceSnapshotTask(
	ctx context.Context,
	userCred mcclient.TokenCredential,
	pendingUsage quotas.IQuota,
	parentTaskId string,
) error {
	if task, err := taskman.TaskManager.NewTask(
		ctx, "InstanceSnapshotCreateTask", self, userCred, nil, parentTaskId, "", pendingUsage); err != nil {
		return err
	} else {
		task.ScheduleRun(nil)
	}
	return nil
}

func (manager *SInstanceSnapshotManager) fillInstanceSnapshot(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, instanceSnapshot *SInstanceSnapshot) {
	instanceSnapshot.SetModelManager(manager, instanceSnapshot)
	instanceSnapshot.ProjectId = guest.ProjectId
	instanceSnapshot.DomainId = guest.DomainId
	instanceSnapshot.GuestId = guest.Id
	instanceSnapshot.InstanceType = guest.InstanceType
	instanceSnapshot.ImageId = guest.GetTemplateId()

	// inherit encrypt_key_id from guest
	instanceSnapshot.EncryptKeyId = guest.EncryptKeyId

	guestSchedInput := guest.ToSchedDesc()

	host, _ := guest.GetHost()
	instanceSnapshot.ManagerId = host.ManagerId
	zone, _ := host.GetZone()
	instanceSnapshot.CloudregionId = zone.CloudregionId

	for i := 0; i < len(guestSchedInput.Disks); i++ {
		guestSchedInput.Disks[i].ImageId = ""
	}
	guestSchedInput.Name = ""
	guestSchedInput.HostId = ""
	guestSchedInput.Project = ""
	guestSchedInput.Domain = ""
	for i := 0; i < len(guestSchedInput.Networks); i++ {
		guestSchedInput.Networks[i].Mac = ""
		guestSchedInput.Networks[i].Address = ""
		guestSchedInput.Networks[i].Address6 = ""
	}
	instanceSnapshot.ServerConfig = jsonutils.Marshal(guestSchedInput.ServerConfig)
	if len(guest.KeypairId) > 0 {
		keypair, _ := KeypairManager.FetchById(guest.KeypairId)
		if keypair != nil {
			instanceSnapshot.KeypairId = guest.KeypairId
		}
	}
	serverMetadata := jsonutils.NewDict()
	if loginAccount := guest.GetMetadata(ctx, "login_account", nil); len(loginAccount) > 0 {
		loginKey := guest.GetMetadata(ctx, "login_key", nil)
		if len(guest.KeypairId) == 0 && len(loginKey) > 0 {
			passwd, e := utils.DescryptAESBase64(guest.Id, loginKey)
			if e == nil {
				serverMetadata.Set("login_account", jsonutils.NewString(loginAccount))
				serverMetadata.Set("passwd", jsonutils.NewString(passwd))
			}
		} else {
			serverMetadata.Set("login_key", jsonutils.NewString(loginKey))
			serverMetadata.Set("login_account", jsonutils.NewString(loginAccount))
		}
	}
	if osArch := guest.GetMetadata(ctx, "os_arch", nil); len(osArch) > 0 {
		serverMetadata.Set("os_arch", jsonutils.NewString(osArch))
	}
	if osDist := guest.GetMetadata(ctx, "os_distribution", nil); len(osDist) > 0 {
		serverMetadata.Set("os_distribution", jsonutils.NewString(osDist))
	}
	if osName := guest.GetMetadata(ctx, "os_name", nil); len(osName) > 0 {
		serverMetadata.Set("os_name", jsonutils.NewString(osName))
	}
	if osVersion := guest.GetMetadata(ctx, "os_version", nil); len(osVersion) > 0 {
		serverMetadata.Set("os_version", jsonutils.NewString(osVersion))
	}
	secs, _ := guest.GetSecgroups()
	if len(secs) > 0 {
		secIds := make([]string, len(secs))
		for i := 0; i < len(secs); i++ {
			secIds[i] = secs[i].Id
		}
		instanceSnapshot.SecGroups = jsonutils.Marshal(secIds)
	}
	instanceSnapshot.OsType = guest.OsType
	instanceSnapshot.OsArch = guest.OsArch
	instanceSnapshot.ServerMetadata = serverMetadata
}

func (manager *SInstanceSnapshotManager) CreateInstanceSnapshot(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, name string, autoDelete bool, withMemory bool) (*SInstanceSnapshot, error) {
	instanceSnapshot := &SInstanceSnapshot{}
	instanceSnapshot.SetModelManager(manager, instanceSnapshot)
	instanceSnapshot.Name = name
	instanceSnapshot.AutoDelete = autoDelete
	if autoDelete {
		// hide auto-delete instance snapshots
		instanceSnapshot.IsSystem = true
	}
	manager.fillInstanceSnapshot(ctx, userCred, guest, instanceSnapshot)
	// compute size of instanceSnapshot
	// instanceSnapshot.SizeMb = guest.getDiskSize()
	instanceSnapshot.WithMemory = withMemory
	instanceSnapshot.MemoryFileHostId = guest.HostId
	err := manager.TableSpec().Insert(ctx, instanceSnapshot)
	if err != nil {
		return nil, errors.Wrap(err, "Insert")
	}
	err = db.InheritFromTo(ctx, userCred, guest, instanceSnapshot)
	if err != nil {
		return nil, errors.Wrap(err, "Inherit ClassMetadata")
	}
	return instanceSnapshot, nil
}

var HypervisorIndependentInstanceSnapshot = []string{
	api.HYPERVISOR_KVM,
}

var ProviderHasSubSnapshot = []string{
	api.CLOUD_PROVIDER_ONECLOUD,
}

func (self *SInstanceSnapshot) ToInstanceCreateInput(
	sourceInput *api.ServerCreateInput) (*api.ServerCreateInput, error) {

	serverConfig := new(schedapi.ServerConfig)
	if err := self.ServerConfig.Unmarshal(serverConfig); err != nil {
		return nil, errors.Wrap(err, "unmarshal sched input")
	}

	provider := self.GetProviderName()
	if utils.IsInStringArray(provider, ProviderHasSubSnapshot) {
		isjs := make([]SInstanceSnapshotJoint, 0)
		err := InstanceSnapshotJointManager.Query().Equals("instance_snapshot_id", self.Id).Asc("disk_index").All(&isjs)
		if err != nil {
			return nil, errors.Wrap(err, "fetch instance snapshots")
		}

		for i := 0; i < len(serverConfig.Disks); i++ {
			index := serverConfig.Disks[i].Index
			if index < len(isjs) {
				serverConfig.Disks[i].SnapshotId = isjs[index].SnapshotId
				if i == 0 && len(self.ImageId) > 0 {
					// system disk, save ImageId
					serverConfig.Disks[i].ImageId = self.ImageId
				}
			}
		}
	}

	sourceInput.Disks = serverConfig.Disks
	if sourceInput.VmemSize == 0 {
		sourceInput.VmemSize = serverConfig.Memory
	}
	if sourceInput.VcpuCount == 0 {
		sourceInput.VcpuCount = serverConfig.Ncpu
	}
	if len(self.KeypairId) > 0 {
		sourceInput.KeypairId = self.KeypairId
	}
	if self.SecGroups != nil {
		secGroups := make([]string, 0)
		inputSecgs := make([]string, 0)
		self.SecGroups.Unmarshal(&secGroups)
		for i := 0; i < len(secGroups); i++ {
			_, err := SecurityGroupManager.FetchSecgroupById(secGroups[i])
			if err == nil {
				inputSecgs = append(inputSecgs, secGroups[i])
			}
		}
		sourceInput.Secgroups = inputSecgs
	}
	sourceInput.OsType = self.OsType
	sourceInput.OsArch = self.OsArch
	sourceInput.InstanceType = self.InstanceType
	if len(sourceInput.Networks) == 0 {
		sourceInput.Networks = serverConfig.Networks
	}

	if self.IsEncrypted() {
		if sourceInput.EncryptKeyId != nil && *sourceInput.EncryptKeyId != self.EncryptKeyId {
			return nil, errors.Wrap(httperrors.ErrConflict, "encrypt_key_id conflict with instance_snapshot's encrypt_key_id")
		}
		sourceInput.EncryptKeyId = &self.EncryptKeyId
	}

	return sourceInput, nil
}

func (self *SInstanceSnapshot) GetSnapshots() ([]SSnapshot, error) {
	isjq := InstanceSnapshotJointManager.Query("snapshot_id").Equals("instance_snapshot_id", self.Id)
	snapshots := make([]SSnapshot, 0)
	err := SnapshotManager.Query().In("id", isjq).All(&snapshots)
	if err != nil && err != sql.ErrNoRows {
		return nil, err
	} else if err != nil && err == sql.ErrNoRows {
		return nil, nil
	} else {
		for i := 0; i < len(snapshots); i++ {
			snapshots[i].SetModelManager(SnapshotManager, &snapshots[i])
		}
		return snapshots, nil
	}
}

func (self *SInstanceSnapshot) GetQuotaKeys() quotas.IQuotaKeys {
	region, _ := self.GetRegion()
	return fetchRegionalQuotaKeys(
		rbacscope.ScopeProject,
		self.GetOwnerId(),
		region,
		self.GetCloudprovider(),
	)
}

func (self *SInstanceSnapshot) GetUsages() []db.IUsage {
	if self.PendingDeleted || self.Deleted {
		return nil
	}
	usage := SRegionQuota{InstanceSnapshot: 1}
	keys := self.GetQuotaKeys()
	usage.SetKeys(keys)
	return []db.IUsage{
		&usage,
	}
}

func TotalInstanceSnapshotCount(ctx context.Context, scope rbacscope.TRbacScope, ownerId mcclient.IIdentityProvider, rangeObjs []db.IStandaloneModel, providers []string, brands []string, cloudEnv string, policyResult rbacutils.SPolicyResult) (int, error) {
	q := InstanceSnapshotManager.Query()

	switch scope {
	case rbacscope.ScopeSystem:
	case rbacscope.ScopeDomain:
		q = q.Equals("domain_id", ownerId.GetProjectDomainId())
	case rbacscope.ScopeProject:
		q = q.Equals("tenant_id", ownerId.GetProjectId())
	}

	q = db.ObjectIdQueryWithPolicyResult(ctx, q, InstanceSnapshotManager, policyResult)

	q = RangeObjectsFilter(q, rangeObjs, q.Field("cloudregion_id"), nil, q.Field("manager_id"), nil, nil)
	q = CloudProviderFilter(q, q.Field("manager_id"), providers, brands, cloudEnv)
	return q.CountWithError()
}

func (self *SInstanceSnapshot) GetInstanceSnapshotJointAt(diskIndex int) (*SInstanceSnapshotJoint, error) {
	ispj := new(SInstanceSnapshotJoint)
	err := InstanceSnapshotJointManager.Query().
		Equals("instance_snapshot_id", self.Id).Equals("disk_index", diskIndex).First(ispj)
	return ispj, err
}

func (self *SInstanceSnapshot) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSONObject) error {
	if self.Status == api.INSTANCE_SNAPSHOT_START_DELETE || self.Status == api.INSTANCE_SNAPSHOT_RESET {
		return httperrors.NewForbiddenError("can't delete instance snapshot with wrong status")
	}
	return nil
}

func (self *SInstanceSnapshot) CustomizeDelete(
	ctx context.Context, userCred mcclient.TokenCredential,
	query jsonutils.JSONObject, data jsonutils.JSONObject) error {

	return self.StartInstanceSnapshotDeleteTask(ctx, userCred, "")
}

func (self *SInstanceSnapshot) StartInstanceSnapshotDeleteTask(
	ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error {

	task, err := taskman.TaskManager.NewTask(
		ctx, "InstanceSnapshotDeleteTask", self, userCred, nil, parentTaskId, "", nil)
	if err != nil {
		log.Errorf("%s", err)
		return err
	}
	self.SetStatus(ctx, userCred, api.INSTANCE_SNAPSHOT_START_DELETE, "InstanceSnapshotDeleteTask")
	task.ScheduleRun(nil)
	return nil
}

func (self *SInstanceSnapshot) PerformPurge(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
	snapshots, err := self.GetSnapshots()
	if err != nil {
		return nil, err
	}
	for i := range snapshots {
		snapshotId := snapshots[i].Id
		isjp := new(SInstanceSnapshotJoint)
		err = InstanceSnapshotJointManager.Query().
			Equals("instance_snapshot_id", self.Id).Equals("snapshot_id", snapshotId).First(isjp)
		if err == nil || isjp != nil {
			isjp.SetModelManager(InstanceSnapshotJointManager, isjp)
			err = isjp.Delete(ctx, userCred)
			if err != nil {
				return nil, errors.Wrapf(err, "delete instance snapshot joint: %s", snapshotId)
			}
		} else {
			log.Errorf("failed get instance_snapshot %s join %s: %s", self.Id, snapshotId, err)
		}

		_, err = snapshots[i].PerformPurge(ctx, userCred, query, data)
		if err != nil {
			return nil, errors.Wrapf(err, "delete snapshot: %s", snapshotId)
		}
	}
	err = self.RealDelete(ctx, userCred)
	return nil, err
}

func (self *SInstanceSnapshot) RealDelete(ctx context.Context, userCred mcclient.TokenCredential) error {
	return db.DeleteModel(ctx, userCred, self)
}

func (self *SInstanceSnapshot) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
	return nil
}

func (self *SInstanceSnapshot) AddRefCount(ctx context.Context) error {
	lockman.LockObject(ctx, self)
	defer lockman.ReleaseObject(ctx, self)
	_, err := db.Update(self, func() error {
		self.RefCount += 1
		return nil
	})
	return err
}

func (self *SInstanceSnapshot) DecRefCount(ctx context.Context, userCred mcclient.TokenCredential) error {
	lockman.LockObject(ctx, self)
	defer lockman.ReleaseObject(ctx, self)
	_, err := db.Update(self, func() error {
		self.RefCount -= 1
		return nil
	})
	if err == nil && self.RefCount == 0 && self.AutoDelete {
		self.StartInstanceSnapshotDeleteTask(ctx, userCred, "")
	}
	return err
}

func (is *SInstanceSnapshot) syncRemoveCloudInstanceSnapshot(ctx context.Context, userCred mcclient.TokenCredential) error {
	lockman.LockObject(ctx, is)
	defer lockman.ReleaseObject(ctx, is)

	err := is.ValidateDeleteCondition(ctx, nil)
	if err != nil {
		err = is.SetStatus(ctx, userCred, api.INSTANCE_SNAPSHOT_UNKNOWN, "sync to delete")
	} else {
		err = is.RealDelete(ctx, userCred)
	}
	return err
}

func (is *SInstanceSnapshot) SyncWithCloudInstanceSnapshot(ctx context.Context, userCred mcclient.TokenCredential, ext cloudprovider.ICloudInstanceSnapshot, guest *SGuest) error {
	diff, err := db.UpdateWithLock(ctx, is, func() error {
		is.Status = ext.GetStatus()
		InstanceSnapshotManager.fillInstanceSnapshot(ctx, userCred, guest, is)
		return nil
	})
	if err != nil {
		return err
	}
	db.OpsLog.LogSyncUpdate(is, diff, userCred)
	return nil
}

func (manager *SInstanceSnapshotManager) newFromCloudInstanceSnapshot(ctx context.Context, userCred mcclient.TokenCredential, extSnapshot cloudprovider.ICloudInstanceSnapshot, guest *SGuest) (*SInstanceSnapshot, error) {
	instanceSnapshot := SInstanceSnapshot{}
	instanceSnapshot.SetModelManager(manager, &instanceSnapshot)

	instanceSnapshot.ExternalId = extSnapshot.GetGlobalId()
	instanceSnapshot.Status = extSnapshot.GetStatus()
	manager.fillInstanceSnapshot(ctx, userCred, guest, &instanceSnapshot)
	var err = func() error {
		lockman.LockClass(ctx, manager, "name")
		defer lockman.ReleaseClass(ctx, manager, "name")

		newName, err := db.GenerateName(ctx, manager, nil, extSnapshot.GetName())
		if err == nil {
			instanceSnapshot.Name = extSnapshot.GetName()
		} else {
			instanceSnapshot.Name = newName
		}
		return manager.TableSpec().Insert(ctx, &instanceSnapshot)
	}()
	if err != nil {
		return nil, err
	}
	db.OpsLog.LogEvent(&instanceSnapshot, db.ACT_CREATE, instanceSnapshot.GetShortDesc(ctx), userCred)
	return &instanceSnapshot, nil
}

func (self *SInstanceSnapshot) GetRegionDriver() IRegionDriver {
	provider := self.GetProviderName()
	return GetRegionDriver(provider)
}

func (ism *SInstanceSnapshotManager) InitializeData() error {
	q := ism.Query().IsNullOrEmpty("cloudregion_id")
	var isps []SInstanceSnapshot
	err := db.FetchModelObjects(ism, q, &isps)
	if err != nil {
		return errors.Wrap(err, "unable to FetchModelObjects")
	}
	var (
		cloudregionId string
	)
	for i := range isps {
		isp := &isps[i]
		guest, err := isp.GetGuest()
		if errors.Cause(err) == errors.ErrNotFound {
			cloudregionId = api.DEFAULT_REGION_ID
		}
		if err != nil {
			return errors.Wrapf(err, "unable to GetGuest for isp %q", isp.GetId())
		} else {
			host, _ := guest.GetHost()
			zone, _ := host.GetZone()
			cloudregionId = zone.CloudregionId
		}
		_, err = db.Update(isp, func() error {
			isp.CloudregionId = cloudregionId
			return nil
		})
		if err != nil {
			return errors.Wrap(err, "unable to Update db")
		}
	}
	return nil
}

func (isp *SInstanceSnapshot) GetInstanceSnapshotJointsByOrder(guest *SGuest) ([]*SInstanceSnapshotJoint, error) {
	disks, err := guest.GetGuestDisks()
	if err != nil {
		return nil, errors.Wrap(err, "GetGuestDisks")
	}
	ss, err := isp.GetSnapshots()
	if err != nil {
		return nil, errors.Wrapf(err, "Get %s subsnapshots", isp.GetName())
	}
	jIsps := make([]*SInstanceSnapshotJoint, 0)
	for idx, gd := range disks {
		d := gd.GetDisk()
		if d == nil {
			return nil, errors.Wrapf(err, "Not get guestdisk %d related disk", idx)
		}
		if idx >= len(ss) {
			break
		}
		jIsp, err := isp.GetInstanceSnapshotJointAt(idx)
		if err != nil {
			return nil, errors.Wrapf(err, "GetInstanceSnapshotJointAt %d", idx)
		}
		sd, err := ss[idx].GetDisk()
		if err != nil {
			return nil, errors.Wrapf(err, "Get snapshot %d disk", idx)
		}
		if ss[idx].GetId() != jIsp.SnapshotId {
			return nil, errors.Wrapf(err, "InstanceSnapshotJoint %d snapshot_id %q != %q", idx, jIsp.SnapshotId, ss[idx].GetId())
		}
		if sd.GetId() != d.GetId() {
			return nil, errors.Wrapf(err, "Disk Snapshot %d's disk id %q != current disk %q", idx, sd.GetId(), d.GetId())
		}
		jIsps = append(jIsps, jIsp)
	}
	return jIsps, nil
}

func (self *SInstanceSnapshot) CustomizeCreate(
	ctx context.Context,
	userCred mcclient.TokenCredential,
	ownerId mcclient.IIdentityProvider,
	query jsonutils.JSONObject,
	data jsonutils.JSONObject,
) error {
	// use disk's ownerId instead of default ownerId
	guestObj, err := GuestManager.FetchById(self.GuestId)
	if err != nil {
		return errors.Wrap(err, "GuestManager.FetchById")
	}
	ownerId = guestObj.(*SGuest).GetOwnerId()
	return self.SVirtualResourceBase.CustomizeCreate(ctx, userCred, ownerId, query, data)
}

func (manager *SInstanceSnapshotManager) GetNeedAutoSnapshotServers() ([]SSnapshotPolicyResource, error) {
	tz, _ := time.LoadLocation(options.Options.TimeZone)
	t := time.Now().In(tz)
	week := t.Weekday()
	if week == 0 { // sunday is zero
		week += 7
	}
	timePoint := t.Hour()

	policy := SnapshotPolicyManager.Query().Equals("type", api.SNAPSHOT_POLICY_TYPE_SERVER).Equals("cloudregion_id", api.DEFAULT_REGION_ID)
	policy = policy.Filter(sqlchemy.Contains(policy.Field("repeat_weekdays"), fmt.Sprintf("%d", week)))
	sq := policy.Filter(
		sqlchemy.OR(
			sqlchemy.Contains(policy.Field("time_points"), fmt.Sprintf(",%d,", timePoint)),
			sqlchemy.Startswith(policy.Field("time_points"), fmt.Sprintf("[%d,", timePoint)),
			sqlchemy.Endswith(policy.Field("time_points"), fmt.Sprintf(",%d]", timePoint)),
			sqlchemy.Equals(policy.Field("time_points"), fmt.Sprintf("[%d]", timePoint)),
		),
	).SubQuery()
	servers := GuestManager.Query().SubQuery()
	q := SnapshotPolicyResourceManager.Query().Equals("resource_type", api.SNAPSHOT_POLICY_TYPE_SERVER)
	q = q.Join(sq, sqlchemy.Equals(q.Field("snapshotpolicy_id"), sq.Field("id")))
	q = q.Join(servers, sqlchemy.Equals(q.Field("resource_id"), servers.Field("id")))
	ret := []SSnapshotPolicyResource{}
	err := db.FetchModelObjects(SnapshotPolicyResourceManager, q, &ret)
	if err != nil {
		return nil, err
	}
	return ret, nil
}

func (manager *SInstanceSnapshotManager) AutoServerSnapshot(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) {
	servers, err := manager.GetNeedAutoSnapshotServers()
	if err != nil {
		log.Errorf("Get auto snapshot servers id failed: %s", err)
		return
	}
	log.Infof("auto snapshot %d servers", len(servers))

	serverMap := map[string]*SGuest{}
	for i := range servers {
		server, err := servers[i].GetServer()
		if err != nil {
			log.Errorf("get server error: %v", err)
			continue
		}
		serverMap[server.Id] = server
	}
	for i := range serverMap {
		input := api.ServerInstanceSnapshot{}
		input.GenerateName = fmt.Sprintf("auto-%s-%d", serverMap[i].Name, time.Now().Unix())
		serverMap[i].PerformInstanceSnapshot(ctx, userCred, jsonutils.NewDict(), input)
	}
}

var instanceSnapshotCleanupTaskRunning int32 = 0

func (manager *SInstanceSnapshotManager) CleanupInstanceSnapshots(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) {
	if instanceSnapshotCleanupTaskRunning > 0 {
		log.Errorf("Previous CleanupInstanceSnapshots tasks still running !!!")
		return
	}
	instanceSnapshotCleanupTaskRunning = 1
	defer func() {
		instanceSnapshotCleanupTaskRunning = 0
	}()
	sq := manager.Query().Equals("status", api.INSTANCE_SNAPSHOT_READY).Startswith("name", "auto-").SubQuery()

	iss := []struct {
		GuestCnt int
		GuestId  string
	}{}
	q := sq.Query(
		sqlchemy.COUNT("guest_cnt", sq.Field("guest_id")),
		sq.Field("guest_id"),
	).GroupBy(sq.Field("guest_id"))
	err := q.All(&iss)
	if err != nil {
		log.Errorf("Cleanup instance snapshots job fetch instance snapshot failed %s", err)
		return
	}

	guestCount := map[string]int{}
	for i := range iss {
		guestCount[iss[i].GuestId] = iss[i].GuestCnt
	}

	// cleanup retention count instance snapshots
	{
		sq = SnapshotPolicyManager.Query().Equals("type", api.SNAPSHOT_POLICY_TYPE_SERVER).GT("retention_count", 0).SubQuery()
		spr := SnapshotPolicyResourceManager.Query().Equals("resource_type", api.SNAPSHOT_POLICY_TYPE_SERVER).SubQuery()
		q = sq.Query(
			sq.Field("retention_count"),
			spr.Field("resource_id").Label("guest_id"),
		)
		q = q.Join(spr, sqlchemy.Equals(q.Field("id"), spr.Field("snapshotpolicy_id")))

		guestRetentions := []struct {
			GuestId        string
			RetentionCount int
		}{}
		err = q.All(&guestRetentions)
		if err != nil {
			log.Errorf("Cleanup instance snapshots job fetch guest retentions failed %s", err)
			return
		}
		guestRetentionMap := map[string]int{}
		for i := range guestRetentions {
			if _, ok := guestRetentionMap[guestRetentions[i].GuestId]; !ok {
				guestRetentionMap[guestRetentions[i].GuestId] = guestRetentions[i].RetentionCount
			}
			// 取最小保留个数
			if guestRetentionMap[guestRetentions[i].GuestId] > guestRetentions[i].RetentionCount {
				guestRetentionMap[guestRetentions[i].GuestId] = guestRetentions[i].RetentionCount
			}
		}

		for guestId, retentionCnt := range guestRetentionMap {
			if cnt, ok := guestCount[guestId]; ok && cnt > retentionCnt {
				manager.startCleanupRetentionCount(ctx, userCred, guestId, cnt-retentionCnt)
			}
		}
	}

	// cleanup retention days instance snapshots
	{
		sq = SnapshotPolicyManager.Query().Equals("type", api.SNAPSHOT_POLICY_TYPE_SERVER).GT("retention_days", 0).SubQuery()
		spr := SnapshotPolicyResourceManager.Query().Equals("resource_type", api.SNAPSHOT_POLICY_TYPE_SERVER).SubQuery()
		q = sq.Query(
			sq.Field("retention_days"),
			spr.Field("resource_id").Label("guest_id"),
		)
		q = q.Join(spr, sqlchemy.Equals(q.Field("id"), spr.Field("snapshotpolicy_id")))

		guestRetentions := []struct {
			GuestId       string
			RetentionDays int
		}{}
		err = q.All(&guestRetentions)
		if err != nil {
			log.Errorf("Cleanup instance snapshots job fetch guest retentions failed %s", err)
			return
		}
		guestRetentionMap := map[string]int{}
		for i := range guestRetentions {
			if _, ok := guestRetentionMap[guestRetentions[i].GuestId]; !ok {
				guestRetentionMap[guestRetentions[i].GuestId] = guestRetentions[i].RetentionDays
			}
			// 取最小保留天数
			if guestRetentionMap[guestRetentions[i].GuestId] > guestRetentions[i].RetentionDays {
				guestRetentionMap[guestRetentions[i].GuestId] = guestRetentions[i].RetentionDays
			}
		}
		for guestId, retentionDays := range guestRetentionMap {
			manager.startCleanupRetentionDays(ctx, userCred, guestId, retentionDays)
		}
	}
}

func (manager *SInstanceSnapshotManager) startCleanupRetentionCount(ctx context.Context, userCred mcclient.TokenCredential, guestId string, cnt int) error {
	q := manager.Query().Equals("guest_id", guestId).Equals("status", api.INSTANCE_SNAPSHOT_READY).Startswith("name", "auto-").Asc("created_at").Limit(cnt)
	vms := []SInstanceSnapshot{}
	err := db.FetchModelObjects(manager, q, &vms)
	if err != nil {
		return errors.Wrapf(err, "FetchModelObjects")
	}
	for i := range vms {
		vms[i].StartInstanceSnapshotDeleteTask(ctx, userCred, "")
	}
	return nil
}

func (manager *SInstanceSnapshotManager) startCleanupRetentionDays(ctx context.Context, userCred mcclient.TokenCredential, guestId string, day int) error {
	expiredTime := time.Now().AddDate(0, 0, -day)
	q := manager.Query().Equals("guest_id", guestId).Equals("status", api.INSTANCE_SNAPSHOT_READY).Startswith("name", "auto-").LE("created_at", expiredTime)
	vms := []SInstanceSnapshot{}
	err := db.FetchModelObjects(manager, q, &vms)
	if err != nil {
		return errors.Wrapf(err, "FetchModelObjects")
	}
	for i := range vms {
		vms[i].StartInstanceSnapshotDeleteTask(ctx, userCred, "")
	}
	return nil
}
