// 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"
	"fmt"
	"strconv"
	"strings"
	"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/delayedwork"
	"yunion.io/x/pkg/util/rbacscope"
	"yunion.io/x/sqlchemy"

	api "yunion.io/x/onecloud/pkg/apis/compute"
	"yunion.io/x/onecloud/pkg/cloudcommon/db"
	"yunion.io/x/onecloud/pkg/cloudcommon/db/lockman"
	"yunion.io/x/onecloud/pkg/httperrors"
	"yunion.io/x/onecloud/pkg/mcclient"
	"yunion.io/x/onecloud/pkg/mcclient/auth"
	ctxutils "yunion.io/x/onecloud/pkg/util/ctx"
	"yunion.io/x/onecloud/pkg/util/stringutils2"
)

// +onecloud:swagger-gen-model-singular=networkaddress
// +onecloud:swagger-gen-model-plural=networkaddresses
type SNetworkAddressManager struct {
	db.SStandaloneAnonResourceBaseManager
	SNetworkResourceBaseManager

	delayedWorkManager *delayedwork.DelayedWorkManager
}

var NetworkAddressManager *SNetworkAddressManager

func init() {
	NetworkAddressManager = &SNetworkAddressManager{
		SStandaloneAnonResourceBaseManager: db.NewStandaloneAnonResourceBaseManager(
			SNetworkAddress{},
			"networkaddresses_tbl",
			"networkaddress",
			"networkaddresses",
		),
	}
	NetworkAddressManager.SetVirtualObject(NetworkAddressManager)
	NetworkAddressManager.delayedWorkManager = delayedwork.NewDelayedWorkManager()
}

type SNetworkAddress struct {
	db.SStandaloneAnonResourceBase

	Type       string                        `width:"16" charset:"ascii" list:"user" create:"required"`
	ParentType api.TNetworkAddressParentType `width:"16" charset:"ascii" list:"user" create:"required"`
	ParentId   string                        `width:"16" charset:"ascii" list:"user" create:"optional"`
	SNetworkResourceBase
	IpAddr string `width:"16" charset:"ascii" list:"user" create:"optional"`

	SubCtrVid int `create:"optional"`
}

func (man *SNetworkAddressManager) InitializeData() error {
	go man.delayedWorkManager.Start(context.Background())
	{
		err := man.cleanDeletedGuestnetworkSubIPs()
		if err != nil {
			return errors.Wrapf(err, "cleanDeletedGuestnetworkSubIPs")
		}
	}
	return nil
}

func (man *SNetworkAddressManager) queryByParentTypeId(typ api.TNetworkAddressParentType, id string) *sqlchemy.SQuery {
	q := NetworkAddressManager.Query().
		Equals("parent_type", string(typ)).
		Equals("parent_id", id)
	return q
}

func (man *SNetworkAddressManager) queryByGuestnetworkId(rowid int64) *sqlchemy.SQuery {
	id := strconv.FormatInt(rowid, 10)
	return man.queryByParentTypeId(api.NetworkAddressParentTypeGuestnetwork, id)
}

func (man *SNetworkAddressManager) fetchByParentTypeId(typ api.TNetworkAddressParentType, id string) ([]SNetworkAddress, error) {
	var (
		q   = man.queryByParentTypeId(typ, id)
		nas []SNetworkAddress
	)
	if err := db.FetchModelObjects(man, q, &nas); err != nil {
		return nil, err
	}
	return nas, nil
}

func (man *SNetworkAddressManager) fetchByGuestnetworkId(rowid int64) ([]SNetworkAddress, error) {
	id := strconv.FormatInt(rowid, 10)
	return man.fetchByParentTypeId(api.NetworkAddressParentTypeGuestnetwork, id)
}

func (man *SNetworkAddressManager) fetchAddressCountByGuestnetworkId(rowid int64) (int, error) {
	q := man.queryByGuestnetworkId(rowid)
	return q.CountWithError()
}

func (man *SNetworkAddressManager) fetchAddressesByGuestnetworkId(rowid int64) ([]api.NetworkAddrConf, error) {
	var (
		naq     = man.queryByGuestnetworkId(rowid).SubQuery()
		nq      = NetworkManager.Query().SubQuery()
		ipnetsq = naq.Query(
			naq.Field("id"),
			naq.Field("type"),
			naq.Field("ip_addr"),
			nq.Field("guest_ip_mask").Label("masklen"),
			nq.Field("guest_gateway").Label("gateway"),
		).Join(nq, sqlchemy.Equals(
			naq.Field("network_id"),
			nq.Field("id")),
		)
		ipnets []api.NetworkAddrConf
	)
	if err := ipnetsq.All(&ipnets); err != nil {
		return nil, errors.Wrapf(err, "fetch addresses ipnets by guestnetwork row id: %d", rowid)
	}
	return ipnets, nil
}

func (man *SNetworkAddressManager) deleteByGuestnetworkId(ctx context.Context, userCred mcclient.TokenCredential, rowid int64) error {
	nas, err := NetworkAddressManager.fetchByGuestnetworkId(rowid)
	if err != nil {
		return errors.Wrap(err, "fetch attached network addresses")
	}
	var errs []error
	for i := range nas {
		na := &nas[i]
		if err := na.remoteUnassignAddress(ctx, userCred); err != nil {
			errs = append(errs, err)
			continue
		}
		if err := db.DeleteModel(ctx, userCred, na); err != nil {
			errs = append(errs, err)
			continue
		}
	}
	return errors.NewAggregate(errs)
}

func (man *SNetworkAddressManager) syncGuestnetworkICloudNic(ctx context.Context, userCred mcclient.TokenCredential, guestnetwork *SGuestnetwork, iNic cloudprovider.ICloudNic) error {
	ipAddrs, err := iNic.GetSubAddress()
	if err != nil {
		return errors.Wrap(err, "iNic.GetSubAddress")
	}
	if err := man.syncGuestnetworkSubIPs(ctx, userCred, guestnetwork, ipAddrs); err != nil {
		return errors.Wrap(err, "syncGuestnetworkSubIPs")
	}
	return nil
}

func (man *SNetworkAddressManager) syncGuestnetworkSubIPs(ctx context.Context, userCred mcclient.TokenCredential, guestnetwork *SGuestnetwork, ipAddrs []string) error {
	nas, err := man.fetchByGuestnetworkId(guestnetwork.RowId)
	if err != nil {
		return errors.Wrap(err, "fetchByGuestnetworkId")
	}

	existings := stringutils2.NewSortedStrings(nil)
	for i := range nas {
		existings = existings.Append(nas[i].IpAddr)
	}
	probed := stringutils2.NewSortedStrings(ipAddrs)

	removes, _, adds := stringutils2.Split(existings, probed)

	log.Debugf("syncGuestnetworkSubIPs removes: %s add %s", jsonutils.Marshal(removes), jsonutils.Marshal(adds))

	if err := man.removeGuestnetworkSubIPs(ctx, userCred, guestnetwork, removes); err != nil {
		return err
	}
	if _, err := man.addGuestnetworkSubIPs(ctx, userCred, guestnetwork, api.GuestAddSubIpsInfo{
		SubIps:   adds,
		Reserved: true,
	}); err != nil {
		return err
	}
	return nil
}

func (man *SNetworkAddressManager) removeGuestnetworkSubIPs(ctx context.Context, userCred mcclient.TokenCredential, guestnetwork *SGuestnetwork, ipAddrs []string) error {
	q := man.queryByGuestnetworkId(guestnetwork.RowId)
	q = q.In("ip_addr", ipAddrs)

	var nas []SNetworkAddress
	if err := db.FetchModelObjects(man, q, &nas); err != nil {
		return errors.Wrapf(err, "removeGuestnetworkSubIPs by ipAddrs %s", strings.Join(ipAddrs, ", "))
	}

	for i := range nas {
		na := &nas[i]
		if err := db.DeleteModel(ctx, userCred, na); err != nil {
			return err
		}
	}
	return nil
}

func (man *SNetworkAddressManager) addGuestnetworkSubIPs(ctx context.Context, userCred mcclient.TokenCredential, guestnetwork *SGuestnetwork, input api.GuestAddSubIpsInfo) ([]string, error) {
	net, err := guestnetwork.GetNetwork()
	if err != nil {
		return nil, errors.Wrapf(err, "GetNetwork")
	}

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

	var (
		usedAddrMap         = net.GetUsedAddresses(ctx)
		recentUsedAddrTable = GuestnetworkManager.getRecentlyReleasedIPAddresses(net.Id, net.getAllocTimoutDuration())
	)

	if input.Count == 0 {
		input.Count = len(input.SubIps)
	}
	if input.Count == 0 {
		// nil operation
		return nil, nil
	}

	addedIps := make([]string, 0)

	errs := make([]error, 0)
	for i := 0; i < input.Count; i++ {
		var candidate string
		if i < len(input.SubIps) {
			candidate = input.SubIps[i]
		}
		ipAddr, err := net.GetFreeIP(ctx, userCred, usedAddrMap, recentUsedAddrTable, candidate, input.AllocDir, input.Reserved, api.AddressTypeIPv4)
		if err != nil {
			errs = append(errs, errors.Wrap(err, "GetFreeIP"))
			continue
		}

		m, err := db.NewModelObject(man)
		if err != nil {
			errs = append(errs, errors.Wrap(err, "NewModelObject"))
			continue
		}
		na := m.(*SNetworkAddress)
		na.NetworkId = guestnetwork.NetworkId
		na.ParentType = api.NetworkAddressParentTypeGuestnetwork
		na.ParentId = strconv.FormatInt(guestnetwork.RowId, 10)
		na.Type = api.NetworkAddressTypeSubIP
		na.IpAddr = ipAddr
		if err := man.TableSpec().Insert(ctx, na); err != nil {
			errs = append(errs, errors.Wrapf(err, "addGuestnetworkSubIPs"))
			continue
		}
		usedAddrMap[ipAddr] = true
		addedIps = append(addedIps, ipAddr)
	}
	if len(errs) > 0 {
		return addedIps, errors.NewAggregate(errs)
	}
	return addedIps, nil
}

func (man *SNetworkAddressManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.NetworkAddressCreateInput) (api.NetworkAddressCreateInput, error) {
	return input, errors.Wrap(httperrors.ErrNotSupported, "no supported")
}

func (na *SNetworkAddress) parentIdInt64() int64 {
	r, err := strconv.ParseInt(na.ParentId, 10, 64)
	if err != nil {
		panic(fmt.Sprintf("networkaddress %s: invalid parent_id %s", na.Id, na.ParentId))
	}
	return r
}

func (na *SNetworkAddress) getGuestnetwork(ctx context.Context, userCred mcclient.TokenCredential) (*SGuestnetwork, error) {
	if na.ParentType == api.NetworkAddressParentTypeGuestnetwork {
		guestnetwork, err := GuestnetworkManager.fetchByRowId(ctx, userCred, na.parentIdInt64())
		if err != nil {
			return nil, err
		}
		return guestnetwork, nil
	}
	return nil, errors.Errorf("getGuestnetwork: expect parent_type %s, got %s", api.NetworkAddressParentTypeGuestnetwork, na.ParentType)
}

func (na *SNetworkAddress) getGuest(ctx context.Context, userCred mcclient.TokenCredential) (*SGuest, error) {
	guestnetwork, err := na.getGuestnetwork(ctx, userCred)
	if err != nil {
		return nil, err
	}
	guest := guestnetwork.GetGuest()
	if guest != nil {
		return guest, nil
	}
	return nil, errors.Wrapf(errors.ErrNotFound, "getGuest: guestnetwork guest_id %s", guestnetwork.GuestId)
}

func (na *SNetworkAddress) getIVM(ctx context.Context, userCred mcclient.TokenCredential) (cloudprovider.ICloudVM, error) {
	guest, err := na.getGuest(ctx, userCred)
	if err != nil {
		return nil, err
	}
	if guest.ExternalId == "" {
		return nil, errors.Errorf("getIVM: guest %s(%s) has empty external_id", guest.Name, guest.Id)
	}
	ivm, err := guest.GetIVM(ctx)
	if err != nil {
		return nil, err
	}
	return ivm, nil
}

func (na *SNetworkAddress) getICloudNic(ctx context.Context, userCred mcclient.TokenCredential) (cloudprovider.ICloudNic, error) {
	guestnetwork, err := na.getGuestnetwork(ctx, userCred)
	if err != nil {
		return nil, err
	}

	iVM, err := na.getIVM(ctx, userCred)
	if err != nil {
		return nil, err
	}
	iNics, err := iVM.GetINics()
	if err != nil {
		return nil, err
	}
	for _, iNic := range iNics {
		if iNic.GetIP() == guestnetwork.IpAddr && iNic.GetMAC() == guestnetwork.MacAddr {
			return iNic, nil
		}
	}
	return nil, errors.Wrapf(errors.ErrNotFound, "getICloudNic: no cloud nic with ip %s, mac %s", guestnetwork.IpAddr, guestnetwork.MacAddr)
}

/*func (na *SNetworkAddress) remoteAssignAddress(ctx context.Context, userCred mcclient.TokenCredential) error {
	if na.ParentType == api.NetworkAddressParentTypeGuestnetwork {
		guest, err := na.getGuest(ctx, userCred)
		if err != nil {
			return err
		}
		if guest.ExternalId != "" {
			iNic, err := na.getICloudNic(ctx, userCred)
			if err != nil {
				return err
			}
			if err := iNic.AssignAddress([]string{na.IpAddr}); err != nil {
				if errors.Cause(err) == cloudprovider.ErrAddressCountExceed {
					return httperrors.NewNotAcceptableError("exceed address count limit: %v", err)
				}
				return err
			}
		} else {
			NetworkAddressManager.submitGuestSyncTask(ctx, userCred, guest)
		}
	}
	return nil
}*/

func (na *SNetworkAddress) remoteUnassignAddress(ctx context.Context, userCred mcclient.TokenCredential) error {
	if na.ParentType == api.NetworkAddressParentTypeGuestnetwork {
		guest, err := na.getGuest(ctx, userCred)
		if err != nil {
			if errors.Cause(err) == errors.ErrNotFound {
				return nil
			}
			return err
		}
		if guest.ExternalId != "" {
			iNic, err := na.getICloudNic(ctx, userCred)
			if err != nil {
				if errors.Cause(err) == errors.ErrNotFound {
					return nil
				}
				return err
			}
			if err := iNic.UnassignAddress([]string{na.IpAddr}); err != nil {
				return err
			}
		} else {
			NetworkAddressManager.submitGuestSyncTask(ctx, userCred, guest)
		}
	}
	return nil
}

func (na *SNetworkAddress) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
	if err := na.remoteUnassignAddress(ctx, userCred); err != nil {
		return err
	}
	return nil
}

func (man *SNetworkAddressManager) ListItemExportKeys(ctx context.Context,
	q *sqlchemy.SQuery,
	userCred mcclient.TokenCredential,
	keys stringutils2.SSortedStrings,
) (*sqlchemy.SQuery, error) {
	return db.ApplyListItemExportKeys(ctx, q, userCred, keys,
		&man.SStandaloneAnonResourceBaseManager,
		&man.SNetworkResourceBaseManager,
	)
}

func (man *SNetworkAddressManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) {
	return db.ApplyQueryDistinctExtraField(q, field,
		&man.SStandaloneAnonResourceBaseManager,
		&man.SNetworkResourceBaseManager,
	)
}

func (man *SNetworkAddressManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.NetworkAddressListInput) (*sqlchemy.SQuery, error) {
	var err error

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

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

	if len(input.GuestId) > 0 {
		gnq := GuestnetworkManager.Query().SubQuery()
		gq := GuestManager.Query().
			In("id", input.GuestId).
			SubQuery()
		idq := man.Query("id").
			Equals("parent_type", api.NetworkAddressParentTypeGuestnetwork)
		idq = idq.Join(gnq, sqlchemy.Equals(gnq.Field("row_id"), idq.Field("parent_id")))
		idq = idq.Join(gq, sqlchemy.Equals(gq.Field("id"), gnq.Field("guest_id")))
		q = q.In("id", idq.SubQuery())
	}

	q, err = managedResourceFilterByAccount(
		ctx,
		q, input.ManagedResourceListInput, "network_id", func() *sqlchemy.SQuery {
			networks := NetworkManager.Query()
			wires := WireManager.Query("id", "vpc_id", "manager_id").SubQuery()
			vpcs := VpcManager.Query("id", "manager_id").SubQuery()
			networks = networks.Join(wires, sqlchemy.Equals(wires.Field("id"), networks.Field("wire_id")))
			networks = networks.Join(vpcs, sqlchemy.Equals(vpcs.Field("id"), wires.Field("vpc_id")))
			networks = networks.AppendField(networks.Field("id"))
			networks = networks.AppendField(sqlchemy.NewFunction(sqlchemy.NewCase().When(sqlchemy.IsNullOrEmpty(wires.Field("manager_id")), vpcs.Field("manager_id")).Else(wires.Field("manager_id")), "manager_id", false))
			subq := networks.SubQuery().Query()
			subq = subq.AppendField(subq.Field("id"))
			return subq
		})
	if err != nil {
		return nil, errors.Wrap(err, "ManagedResourceFilterByAccount")
	}

	return q, nil
}

func (man *SNetworkAddressManager) OrderByExtraFields(
	ctx context.Context,
	q *sqlchemy.SQuery,
	userCred mcclient.TokenCredential,
	query api.NetworkAddressListInput,
) (*sqlchemy.SQuery, error) {
	var err error
	q, err = man.SNetworkResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.NetworkFilterListInput)
	if err != nil {
		return nil, err
	}
	q, err = man.SStandaloneAnonResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.StandaloneAnonResourceListInput)
	if err != nil {
		return nil, err
	}
	return q, nil
}

func (man *SNetworkAddressManager) FetchCustomizeColumns(
	ctx context.Context,
	userCred mcclient.TokenCredential,
	query jsonutils.JSONObject,
	objs []interface{},
	fields stringutils2.SSortedStrings,
	isList bool,
) []api.NetworkAddressDetails {
	ret := make([]api.NetworkAddressDetails, len(objs))
	netCols := man.SNetworkResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
	for i := range netCols {
		ret[i].NetworkResourceInfo = netCols[i]
	}
	stdaCols := man.SStandaloneAnonResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
	for i := range stdaCols {
		ret[i].StandaloneAnonResourceDetails = stdaCols[i]
	}

	{
		guestnetworkRowIds := make([]int64, 0, len(objs))
		for i := range objs {
			na := objs[i].(*SNetworkAddress)
			if na.ParentType == api.NetworkAddressParentTypeGuestnetwork {
				rowId, _ := strconv.ParseInt(na.ParentId, 10, 64)
				guestnetworkRowIds = append(guestnetworkRowIds, rowId)
			}
		}
		if len(guestnetworkRowIds) > 0 {
			gns, err := GuestnetworkManager.fetchByRowIds(ctx, userCred, guestnetworkRowIds)
			if err == nil && len(gns) > 0 {
				gnObjs := make([]interface{}, len(gns))
				for i := range gns {
					gnObjs[i] = &gns[i]
				}
				gnds := GuestnetworkManager.FetchCustomizeColumns(ctx, userCred, query, gnObjs, fields, isList)
				for i, j := 0, 0; i < len(objs); i++ {
					na := objs[i].(*SNetworkAddress)
					if na.ParentType == api.NetworkAddressParentTypeGuestnetwork {
						ret[i].Guestnetwork = gnds[j]
						j++
					}
				}
			}
		}
	}
	return ret
}

func (man *SNetworkAddressManager) FilterByOwner(ctx context.Context, q *sqlchemy.SQuery, manager db.FilterByOwnerProvider, userCred mcclient.TokenCredential, owner mcclient.IIdentityProvider, scope rbacscope.TRbacScope) *sqlchemy.SQuery {
	q = db.ApplyFilterByOwner(ctx, q, userCred, owner, scope,
		&man.SStandaloneAnonResourceBaseManager,
	)
	if owner != nil {
		var condVar, condVal string
		switch scope {
		case rbacscope.ScopeProject:
			condVar, condVal = "tenant_id", owner.GetProjectId()
		case rbacscope.ScopeDomain:
			condVar, condVal = "domain_id", owner.GetProjectDomainId()
		default:
			return q
		}
		{
			var (
				gnq = GuestnetworkManager.Query().SubQuery()
				gq  = GuestManager.Query().SubQuery()
				naq = NetworkAddressManager.Query("id")
			)
			naq = naq.
				Join(gnq, sqlchemy.Equals(naq.Field("parent_id"), gnq.Field("row_id"))).
				Join(gq, sqlchemy.Equals(gnq.Field("guest_id"), gq.Field("id")))
			naq = naq.Equals("parent_type", api.NetworkAddressParentTypeGuestnetwork)
			naq = naq.Filter(sqlchemy.Equals(gq.Field(condVar), condVal))
			q = q.In("id", naq.SubQuery())
		}
	}
	return q
}

func (man *SNetworkAddressManager) submitGuestSyncTask(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest) {
	man.delayedWorkManager.Submit(ctx, delayedwork.DelayedWorkRequest{
		ID:        guest.Id,
		SoftDelay: 2 * time.Second,
		HardDelay: 5 * time.Second,
		Func: func(ctx context.Context) {
			var (
				fwOnly       = false
				parentTaskId = ""
			)
			err := guest.StartSyncTask(ctx, userCred, fwOnly, parentTaskId)
			if err != nil {
				log.Errorf("guest StartSyncTask: %v", err)
			}
		},
	})
}

func (g *SGuest) PerformUpdateSubIps(
	ctx context.Context,
	userCred mcclient.TokenCredential,
	query jsonutils.JSONObject,
	input api.GuestUpdateSubIpsInput,
) (jsonutils.JSONObject, error) {
	gn, err := g.findGuestnetworkByInfo(input.ServerNetworkInfo)
	if err != nil {
		return nil, errors.Wrapf(err, "getGuestnetworkByIpOrMac ip=%s mac=%s", input.IpAddr, input.Mac)
	}

	var iNic cloudprovider.ICloudNic
	if g.ExternalId != "" {
		// sync to cloud
		var err error
		iNic, err = g.getICloudNic(ctx, gn)
		if err != nil {
			return nil, errors.Wrap(err, "getICloudNic")
		}
	}

	errs := make([]error, 0)
	{
		err := NetworkAddressManager.removeGuestnetworkSubIPs(ctx, userCred, gn, input.RemoveSubIps)
		if err != nil {
			errs = append(errs, errors.Wrap(err, "removeGuestnetworkSubIPs"))
		}
	}

	addedIps, err := NetworkAddressManager.addGuestnetworkSubIPs(ctx, userCred, gn, input.GuestAddSubIpsInfo)
	if err != nil {
		errs = append(errs, errors.Wrap(err, "addGuestnetworkSubIPs"))
	}

	if g.ExternalId != "" {
		// sync to cloud
		if len(input.RemoveSubIps) > 0 {
			if err := iNic.UnassignAddress(addedIps); err != nil {
				errs = append(errs, errors.Wrapf(err, "UnassignAddress %s", addedIps))
			}
		}
		if len(addedIps) > 0 {
			if err := iNic.AssignAddress(addedIps); err != nil {
				if errors.Cause(err) == cloudprovider.ErrAddressCountExceed {
					errs = append(errs, httperrors.NewNotAcceptableError("exceed address count limit: %v", err))
				} else {
					errs = append(errs, errors.Wrapf(err, "AssignAddress %s", addedIps))
				}
			}
		}
	} else {
		NetworkAddressManager.submitGuestSyncTask(ctx, userCred, g)
	}

	if len(errs) > 0 {
		return nil, errors.NewAggregate(errs)
	}

	return nil, nil
}

func (g *SGuest) PerformAddSubIps(
	ctx context.Context,
	userCred mcclient.TokenCredential,
	query jsonutils.JSONObject,
	input api.GuestAddSubIpsInput,
) (jsonutils.JSONObject, error) {
	gn, err := g.findGuestnetworkByInfo(input.ServerNetworkInfo)
	if err != nil {
		return nil, errors.Wrapf(err, "getGuestnetworkByIpOrMac ip=%s mac=%s", input.IpAddr, input.Mac)
	}

	addedIps, err := NetworkAddressManager.addGuestnetworkSubIPs(ctx, userCred, gn, input.GuestAddSubIpsInfo)
	if err != nil {
		return nil, errors.Wrap(err, "addGuestnetworkSubIPs")
	}

	if g.ExternalId != "" && len(addedIps) > 0 {
		// sync to cloud
		iNic, err := g.getICloudNic(ctx, gn)
		if err != nil {
			return nil, errors.Wrap(err, "getICloudNic")
		}
		if err := iNic.AssignAddress(addedIps); err != nil {
			if errors.Cause(err) == cloudprovider.ErrAddressCountExceed {
				return nil, httperrors.NewNotAcceptableError("exceed address count limit: %v", err)
			}
			return nil, errors.Wrapf(err, "AssignAddress %s", addedIps)
		}
	} else {
		NetworkAddressManager.submitGuestSyncTask(ctx, userCred, g)
	}

	return nil, nil
}

func (g *SGuest) getICloudNic(ctx context.Context, gn *SGuestnetwork) (cloudprovider.ICloudNic, error) {
	ivm, err := g.GetIVM(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "GetIVM")
	}
	iNics, err := ivm.GetINics()
	if err != nil {
		return nil, errors.Wrap(err, "GetINics %s")
	}
	for _, iNic := range iNics {
		if iNic.GetIP() == gn.IpAddr && iNic.GetMAC() == gn.MacAddr {
			return iNic, nil
		}
	}
	return nil, errors.Wrapf(errors.ErrNotFound, "no nic of ip %s mac %s", gn.IpAddr, gn.MacAddr)
}

func (manager *SNetworkAddressManager) fetchSubIpsQuery(parentType api.TNetworkAddressParentType) *sqlchemy.SQuery {
	subIPQ := manager.Query("parent_id").Equals("parent_type", string(parentType))
	subIPQ = subIPQ.AppendField(sqlchemy.GROUP_CONCAT("sub_ips", subIPQ.Field("ip_addr")))
	subIPQ = subIPQ.GroupBy(subIPQ.Field("parent_id"))
	return subIPQ
}

func (manager *SNetworkAddressManager) cleanDeletedGuestnetworkSubIPs() error {
	ctx := ctxutils.CtxWithTime()
	userCred := auth.AdminCredential()
	gnQ := GuestnetworkManager.Query("row_id").SubQuery()
	q := manager.Query().Equals("parent_type", api.NetworkAddressParentTypeGuestnetwork)
	q = q.LeftJoin(gnQ, sqlchemy.Equals(q.Field("parent_id"), gnQ.Field("row_id")))
	q = q.Filter(sqlchemy.IsNull(gnQ.Field("row_id")))
	netaddrs := make([]SNetworkAddress, 0)
	err := db.FetchModelObjects(manager, q, &netaddrs)
	if err != nil {
		return errors.Wrapf(err, "FetchModelObjects")
	}
	for i := range netaddrs {
		na := &netaddrs[i]
		if err := db.DeleteModel(ctx, userCred, na); err != nil {
			return errors.Wrapf(err, "DeleteModel")
		}
	}
	return nil
}
