// 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 org.apache.cloudstack.region.gslb;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.ejb.Local;
import javax.inject.Inject;

import org.apache.log4j.Logger;

import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.api.command.user.region.ha.gslb.AssignToGlobalLoadBalancerRuleCmd;
import org.apache.cloudstack.api.command.user.region.ha.gslb.CreateGlobalLoadBalancerRuleCmd;
import org.apache.cloudstack.api.command.user.region.ha.gslb.DeleteGlobalLoadBalancerRuleCmd;
import org.apache.cloudstack.api.command.user.region.ha.gslb.ListGlobalLoadBalancerRuleCmd;
import org.apache.cloudstack.api.command.user.region.ha.gslb.RemoveFromGlobalLoadBalancerRuleCmd;
import org.apache.cloudstack.api.command.user.region.ha.gslb.UpdateGlobalLoadBalancerRuleCmd;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.region.Region;
import org.apache.cloudstack.region.dao.RegionDao;

import com.cloud.agent.AgentManager;
import com.cloud.agent.api.routing.GlobalLoadBalancerConfigCommand;
import com.cloud.agent.api.routing.SiteLoadBalancerConfig;
import com.cloud.configuration.Config;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.network.Network;
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.LoadBalancerDao;
import com.cloud.network.dao.LoadBalancerVO;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.rules.LoadBalancer;
import com.cloud.network.rules.RulesManager;
import com.cloud.region.ha.GlobalLoadBalancerRule;
import com.cloud.region.ha.GlobalLoadBalancingRulesService;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.utils.Pair;
import com.cloud.utils.db.DB;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.db.TransactionCallbackNoReturn;
import com.cloud.utils.db.TransactionStatus;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils;

@Local(value = {GlobalLoadBalancingRulesService.class})
public class GlobalLoadBalancingRulesServiceImpl implements GlobalLoadBalancingRulesService {

    private static final Logger s_logger = Logger.getLogger(GlobalLoadBalancingRulesServiceImpl.class);

    @Inject
    AccountManager _accountMgr;
    @Inject
    GlobalLoadBalancerRuleDao _gslbRuleDao;
    @Inject
    GlobalLoadBalancerLbRuleMapDao _gslbLbMapDao;
    @Inject
    RegionDao _regionDao;
    @Inject
    RulesManager _rulesMgr;
    @Inject
    LoadBalancerDao _lbDao;
    @Inject
    NetworkDao _networkDao;
    @Inject
    ConfigurationDao _globalConfigDao;
    @Inject
    IPAddressDao _ipAddressDao;
    @Inject
    AgentManager _agentMgr;

    protected List<GslbServiceProvider> _gslbProviders;

    public void setGslbServiceProviders(List<GslbServiceProvider> providers) {
        _gslbProviders = providers;
    }

    @Override
    @DB
    @ActionEvent(eventType = EventTypes.EVENT_GLOBAL_LOAD_BALANCER_CREATE, eventDescription = "creating global load " + "balancer rule", create = true)
    public GlobalLoadBalancerRule createGlobalLoadBalancerRule(CreateGlobalLoadBalancerRuleCmd newRule) {

        final Integer regionId = newRule.getRegionId();
        final String algorithm = newRule.getAlgorithm();
        final String stickyMethod = newRule.getStickyMethod();
        final String name = newRule.getName();
        final String description = newRule.getDescription();
        final String domainName = newRule.getServiceDomainName();
        final String serviceType = newRule.getServiceType();

        final Account gslbOwner = _accountMgr.getAccount(newRule.getEntityOwnerId());

        if (!GlobalLoadBalancerRule.Algorithm.isValidAlgorithm(algorithm)) {
            throw new InvalidParameterValueException("Invalid Algorithm: " + algorithm);
        }

        if (!GlobalLoadBalancerRule.Persistence.isValidPersistence(stickyMethod)) {
            throw new InvalidParameterValueException("Invalid persistence: " + stickyMethod);
        }

        if (!GlobalLoadBalancerRule.ServiceType.isValidServiceType(serviceType)) {
            throw new InvalidParameterValueException("Invalid service type: " + serviceType);
        }

        if (!NetUtils.verifyDomainName(domainName)) {
            throw new InvalidParameterValueException("Invalid domain name : " + domainName);
        }

        GlobalLoadBalancerRuleVO gslbRuleWithDomainName = _gslbRuleDao.findByDomainName(domainName);
        if (gslbRuleWithDomainName != null) {
            throw new InvalidParameterValueException("Domain name " + domainName + "is in use");
        }

        Region region = _regionDao.findById(regionId);
        if (region == null) {
            throw new InvalidParameterValueException("Invalid region ID: " + regionId);
        }

        String providerDnsName = _globalConfigDao.getValue(Config.CloudDnsName.key());
        if (!region.checkIfServiceEnabled(Region.Service.Gslb) || (providerDnsName == null)) {
            throw new CloudRuntimeException("GSLB service is not enabled in region : " + region.getName());
        }

        GlobalLoadBalancerRuleVO newGslbRule = Transaction.execute(new TransactionCallback<GlobalLoadBalancerRuleVO>() {
            @Override
            public GlobalLoadBalancerRuleVO doInTransaction(TransactionStatus status) {
                GlobalLoadBalancerRuleVO newGslbRule =
                    new GlobalLoadBalancerRuleVO(name, description, domainName, algorithm, stickyMethod, serviceType, regionId, gslbOwner.getId(),
                        gslbOwner.getDomainId(), GlobalLoadBalancerRule.State.Staged);
                _gslbRuleDao.persist(newGslbRule);

                UsageEventUtils.publishUsageEvent(EventTypes.EVENT_GLOBAL_LOAD_BALANCER_CREATE, newGslbRule.getAccountId(), 0, newGslbRule.getId(), name,
                    GlobalLoadBalancerRule.class.getName(), newGslbRule.getUuid());

                return newGslbRule;
            }
        });

        s_logger.debug("successfully created new global load balancer rule for the account " + gslbOwner.getId());

        return newGslbRule;
    }

    @Override
    @DB
    @ActionEvent(eventType = EventTypes.EVENT_ASSIGN_TO_GLOBAL_LOAD_BALANCER_RULE,
                 eventDescription = "Assigning a load balancer rule to global load balancer rule",
                 async = true)
    public boolean assignToGlobalLoadBalancerRule(AssignToGlobalLoadBalancerRuleCmd assignToGslbCmd) {

        CallContext ctx = CallContext.current();
        Account caller = ctx.getCallingAccount();

        final long gslbRuleId = assignToGslbCmd.getGlobalLoadBalancerRuleId();
        final GlobalLoadBalancerRuleVO gslbRule = _gslbRuleDao.findById(gslbRuleId);
        if (gslbRule == null) {
            throw new InvalidParameterValueException("Invalid global load balancer rule id: " + gslbRuleId);
        }

        _accountMgr.checkAccess(caller, SecurityChecker.AccessType.OperateEntry, true, gslbRule);

        if (gslbRule.getState() == GlobalLoadBalancerRule.State.Revoke) {
            throw new InvalidParameterValueException("global load balancer rule id: " + gslbRule.getUuid() + " is in revoked state");
        }

        final List<Long> newLbRuleIds = assignToGslbCmd.getLoadBalancerRulesIds();
        if (newLbRuleIds == null || newLbRuleIds.isEmpty()) {
            throw new InvalidParameterValueException("empty list of load balancer rule Ids specified to be assigned" + " global load balancer rule");
        }

        List<Long> oldLbRuleIds = new ArrayList<Long>();
        List<Long> oldZones = new ArrayList<Long>();
        List<Long> newZones = new ArrayList<Long>(oldZones);
        List<Pair<Long, Long>> physcialNetworks = new ArrayList<Pair<Long, Long>>();

        // get the list of load balancer rules id's that are assigned currently to GSLB rule and corresponding zone id's
        List<GlobalLoadBalancerLbRuleMapVO> gslbLbMapVos = _gslbLbMapDao.listByGslbRuleId(gslbRuleId);
        if (gslbLbMapVos != null) {
            for (GlobalLoadBalancerLbRuleMapVO gslbLbMapVo : gslbLbMapVos) {
                LoadBalancerVO loadBalancer = _lbDao.findById(gslbLbMapVo.getLoadBalancerId());
                Network network = _networkDao.findById(loadBalancer.getNetworkId());
                oldZones.add(network.getDataCenterId());
                oldLbRuleIds.add(gslbLbMapVo.getLoadBalancerId());
            }
        }

        /* check each of the load balancer rule id passed in the 'AssignToGlobalLoadBalancerRuleCmd' command is
         *     valid ID
         *     caller has access to the rule
         *     check rule is not revoked
         *     no two rules are in same zone
         *     rule is not already assigned to gslb rule
         */
        for (Long lbRuleId : newLbRuleIds) {

            LoadBalancerVO loadBalancer = _lbDao.findById(lbRuleId);
            if (loadBalancer == null) {
                throw new InvalidParameterValueException("Specified load balancer rule ID does not exist.");
            }

            _accountMgr.checkAccess(caller, null, true, loadBalancer);

            if (gslbRule.getAccountId() != loadBalancer.getAccountId()) {
                throw new InvalidParameterValueException("GSLB rule and load balancer rule does not belong to same account");
            }

            if (loadBalancer.getState() == LoadBalancer.State.Revoke) {
                throw new InvalidParameterValueException("Load balancer ID " + loadBalancer.getUuid() + " is in revoke state");
            }

            if (oldLbRuleIds != null && oldLbRuleIds.contains(loadBalancer.getId())) {
                throw new InvalidParameterValueException("Load balancer ID " + loadBalancer.getUuid() + " is already assigned");
            }

            Network network = _networkDao.findById(loadBalancer.getNetworkId());

            if (oldZones != null && oldZones.contains(network.getDataCenterId()) || newZones != null && newZones.contains(network.getDataCenterId())) {
                throw new InvalidParameterValueException("Load balancer rule specified should be in unique zone");
            }

            newZones.add(network.getDataCenterId());
            physcialNetworks.add(new Pair<Long, Long>(network.getDataCenterId(), network.getPhysicalNetworkId()));
        }

        // for each of the physical network check if GSLB service provider configured
        for (Pair<Long, Long> physicalNetwork : physcialNetworks) {
            if (!checkGslbServiceEnabledInZone(physicalNetwork.first(), physicalNetwork.second())) {
                throw new InvalidParameterValueException("GSLB service is not enabled in the Zone:" + physicalNetwork.first() + " and physical network " +
                    physicalNetwork.second());
            }
        }

        final Map<Long, Long> lbRuleWeightMap = assignToGslbCmd.getLoadBalancerRuleWeightMap();

        Transaction.execute(new TransactionCallbackNoReturn() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus status) {
                // persist the mapping for the new Lb rule that needs to assigned to a gslb rule
                for (Long lbRuleId : newLbRuleIds) {
                    GlobalLoadBalancerLbRuleMapVO newGslbLbMap = new GlobalLoadBalancerLbRuleMapVO();
                    newGslbLbMap.setGslbLoadBalancerId(gslbRuleId);
                    newGslbLbMap.setLoadBalancerId(lbRuleId);
                    if (lbRuleWeightMap != null && lbRuleWeightMap.get(lbRuleId) != null) {
                        newGslbLbMap.setWeight(lbRuleWeightMap.get(lbRuleId));
                    }
                    _gslbLbMapDao.persist(newGslbLbMap);
                }

                // mark the gslb rule state as add
                if (gslbRule.getState() == GlobalLoadBalancerRule.State.Staged || gslbRule.getState() == GlobalLoadBalancerRule.State.Active) {
                    gslbRule.setState(GlobalLoadBalancerRule.State.Add);
                    _gslbRuleDao.update(gslbRule.getId(), gslbRule);
                }
            }
        });

        boolean success = false;
        try {
            s_logger.debug("Configuring gslb rule configuration on the gslb service providers in the participating zones");

            // apply the gslb rule on to the back end gslb service providers on zones participating in gslb
            if (!applyGlobalLoadBalancerRuleConfig(gslbRuleId, false)) {
                s_logger.warn("Failed to add load balancer rules " + newLbRuleIds + " to global load balancer rule id " + gslbRuleId);
                CloudRuntimeException ex = new CloudRuntimeException("Failed to add load balancer rules to GSLB rule ");
                throw ex;
            }

            // on success set state to Active
            gslbRule.setState(GlobalLoadBalancerRule.State.Active);
            _gslbRuleDao.update(gslbRule.getId(), gslbRule);

            success = true;

        } catch (ResourceUnavailableException e) {
            throw new CloudRuntimeException("Failed to apply new GSLB configuration while assigning new LB rules to GSLB rule.");
        }

        return success;
    }

    @Override
    @DB
    @ActionEvent(eventType = EventTypes.EVENT_REMOVE_FROM_GLOBAL_LOAD_BALANCER_RULE,
                 eventDescription = "Removing a load balancer rule to be part of global load balancer rule")
    public boolean removeFromGlobalLoadBalancerRule(RemoveFromGlobalLoadBalancerRuleCmd removeFromGslbCmd) {

        CallContext ctx = CallContext.current();
        Account caller = ctx.getCallingAccount();

        final long gslbRuleId = removeFromGslbCmd.getGlobalLoadBalancerRuleId();
        final GlobalLoadBalancerRuleVO gslbRule = _gslbRuleDao.findById(gslbRuleId);
        if (gslbRule == null) {
            throw new InvalidParameterValueException("Invalid global load balancer rule id: " + gslbRuleId);
        }

        _accountMgr.checkAccess(caller, SecurityChecker.AccessType.OperateEntry, true, gslbRule);

        if (gslbRule.getState() == GlobalLoadBalancerRule.State.Revoke) {
            throw new InvalidParameterValueException("global load balancer rule id: " + gslbRuleId + " is already in revoked state");
        }

        final List<Long> lbRuleIdsToremove = removeFromGslbCmd.getLoadBalancerRulesIds();
        if (lbRuleIdsToremove == null || lbRuleIdsToremove.isEmpty()) {
            throw new InvalidParameterValueException("empty list of load balancer rule Ids specified to be un-assigned" + " to global load balancer rule");
        }

        // get the active list of LB rule id's that are assigned currently to GSLB rule and corresponding zone id's
        List<Long> oldLbRuleIds = new ArrayList<Long>();
        List<Long> oldZones = new ArrayList<Long>();

        List<GlobalLoadBalancerLbRuleMapVO> gslbLbMapVos = _gslbLbMapDao.listByGslbRuleId(gslbRuleId);
        if (gslbLbMapVos == null) {
            throw new InvalidParameterValueException(" There are no load balancer rules that are assigned to global " + " load balancer rule id: " + gslbRule.getUuid() +
                " that are available for deletion");
        }

        for (Long lbRuleId : lbRuleIdsToremove) {
            LoadBalancerVO loadBalancer = _lbDao.findById(lbRuleId);
            if (loadBalancer == null) {
                throw new InvalidParameterValueException("Specified load balancer rule ID does not exist.");
            }

            _accountMgr.checkAccess(caller, null, true, loadBalancer);
        }

        for (GlobalLoadBalancerLbRuleMapVO gslbLbMapVo : gslbLbMapVos) {
            LoadBalancerVO loadBalancer = _lbDao.findById(gslbLbMapVo.getLoadBalancerId());
            Network network = _networkDao.findById(loadBalancer.getNetworkId());
            oldLbRuleIds.add(gslbLbMapVo.getLoadBalancerId());
            oldZones.add(network.getDataCenterId());
        }

        for (Long lbRuleId : lbRuleIdsToremove) {
            LoadBalancerVO loadBalancer = _lbDao.findById(lbRuleId);
            if (oldLbRuleIds != null && !oldLbRuleIds.contains(loadBalancer.getId())) {
                throw new InvalidParameterValueException("Load balancer ID " + loadBalancer.getUuid() + " is not assigned" + " to global load balancer rule: " +
                    gslbRule.getUuid());
            }
        }

        Transaction.execute(new TransactionCallbackNoReturn() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus status) {
                // update the mapping of gslb rule to Lb rule, to revoke state
                for (Long lbRuleId : lbRuleIdsToremove) {
                    GlobalLoadBalancerLbRuleMapVO removeGslbLbMap = _gslbLbMapDao.findByGslbRuleIdAndLbRuleId(gslbRuleId, lbRuleId);
                    removeGslbLbMap.setRevoke(true);
                    _gslbLbMapDao.update(removeGslbLbMap.getId(), removeGslbLbMap);
                }

                // mark the gslb rule state as add
                if (gslbRule.getState() == GlobalLoadBalancerRule.State.Staged) {
                    gslbRule.setState(GlobalLoadBalancerRule.State.Add);
                    _gslbRuleDao.update(gslbRule.getId(), gslbRule);
                }

            }
        });

        boolean success = false;
        try {
            s_logger.debug("Attempting to configure global load balancer rule configuration on the gslb service providers ");

            // apply the gslb rule on to the back end gslb service providers
            if (!applyGlobalLoadBalancerRuleConfig(gslbRuleId, false)) {
                s_logger.warn("Failed to remove load balancer rules " + lbRuleIdsToremove + " from global load balancer rule id " + gslbRuleId);
                CloudRuntimeException ex = new CloudRuntimeException("Failed to remove load balancer rule ids from GSLB rule ");
                throw ex;
            }

            Transaction.execute(new TransactionCallbackNoReturn() {
                @Override
                public void doInTransactionWithoutResult(TransactionStatus status) {
                    // remove the mappings of gslb rule to Lb rule that are in revoked state
                    for (Long lbRuleId : lbRuleIdsToremove) {
                        GlobalLoadBalancerLbRuleMapVO removeGslbLbMap = _gslbLbMapDao.findByGslbRuleIdAndLbRuleId(gslbRuleId, lbRuleId);
                        _gslbLbMapDao.remove(removeGslbLbMap.getId());
                    }

                    // on success set state back to Active
                    gslbRule.setState(GlobalLoadBalancerRule.State.Active);
                    _gslbRuleDao.update(gslbRule.getId(), gslbRule);

                }
            });

            success = true;
        } catch (ResourceUnavailableException e) {
            throw new CloudRuntimeException("Failed to update removed load balancer details from gloabal load balancer");
        }

        return success;
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_GLOBAL_LOAD_BALANCER_DELETE, eventDescription = "Delete global load balancer rule")
    public boolean deleteGlobalLoadBalancerRule(DeleteGlobalLoadBalancerRuleCmd deleteGslbCmd) {

        CallContext ctx = CallContext.current();
        Account caller = ctx.getCallingAccount();
        long gslbRuleId = deleteGslbCmd.getGlobalLoadBalancerId();

        try {
            revokeGslbRule(gslbRuleId, caller);
        } catch (Exception e) {
            s_logger.warn("Failed to delete GSLB rule due to" + e.getMessage());
            return false;
        }

        return true;
    }

    @DB
    private void revokeGslbRule(final long gslbRuleId, Account caller) {

        final GlobalLoadBalancerRuleVO gslbRule = _gslbRuleDao.findById(gslbRuleId);

        if (gslbRule == null) {
            throw new InvalidParameterValueException("Invalid global load balancer rule id: " + gslbRuleId);
        }

        _accountMgr.checkAccess(caller, SecurityChecker.AccessType.OperateEntry, true, gslbRule);

        if (gslbRule.getState() == com.cloud.region.ha.GlobalLoadBalancerRule.State.Staged) {
            if (s_logger.isDebugEnabled()) {
                s_logger.debug("Rule Id: " + gslbRuleId + " is still in Staged state so just removing it.");
            }
            _gslbRuleDao.remove(gslbRuleId);
            UsageEventUtils.publishUsageEvent(EventTypes.EVENT_GLOBAL_LOAD_BALANCER_DELETE, gslbRule.getAccountId(), 0, gslbRule.getId(), gslbRule.getName(),
                GlobalLoadBalancerRule.class.getName(), gslbRule.getUuid());
            return;
        } else if (gslbRule.getState() == GlobalLoadBalancerRule.State.Add || gslbRule.getState() == GlobalLoadBalancerRule.State.Active) {
            //mark the GSlb rule to be in revoke state
            gslbRule.setState(GlobalLoadBalancerRule.State.Revoke);
            _gslbRuleDao.update(gslbRuleId, gslbRule);
        }

        final List<GlobalLoadBalancerLbRuleMapVO> gslbLbMapVos = Transaction.execute(new TransactionCallback<List<GlobalLoadBalancerLbRuleMapVO>>() {
            @Override
            public List<GlobalLoadBalancerLbRuleMapVO> doInTransaction(TransactionStatus status) {
                List<GlobalLoadBalancerLbRuleMapVO> gslbLbMapVos = _gslbLbMapDao.listByGslbRuleId(gslbRuleId);
                if (gslbLbMapVos != null) {
                    //mark all the GSLB-LB mapping to be in revoke state
                    for (GlobalLoadBalancerLbRuleMapVO gslbLbMap : gslbLbMapVos) {
                        gslbLbMap.setRevoke(true);
                        _gslbLbMapDao.update(gslbLbMap.getId(), gslbLbMap);
                    }
                }

                return gslbLbMapVos;
            }
        });

        boolean success = false;
        try {
            if (gslbLbMapVos != null) {
                applyGlobalLoadBalancerRuleConfig(gslbRuleId, true);
            }
            success = true;
        } catch (ResourceUnavailableException e) {
            throw new CloudRuntimeException("Failed to update the gloabal load balancer");
        }

        Transaction.execute(new TransactionCallbackNoReturn() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus status) {
                //remove all mappings between GSLB rule and load balancer rules
                if (gslbLbMapVos != null) {
                    for (GlobalLoadBalancerLbRuleMapVO gslbLbMap : gslbLbMapVos) {
                        _gslbLbMapDao.remove(gslbLbMap.getId());
                    }
                }

                //remove the GSLB rule itself
                _gslbRuleDao.remove(gslbRuleId);

                UsageEventUtils.publishUsageEvent(EventTypes.EVENT_GLOBAL_LOAD_BALANCER_DELETE, gslbRule.getAccountId(), 0, gslbRule.getId(), gslbRule.getName(),
                    GlobalLoadBalancerRule.class.getName(), gslbRule.getUuid());
            }
        });

    }

    @Override
    public GlobalLoadBalancerRule updateGlobalLoadBalancerRule(UpdateGlobalLoadBalancerRuleCmd updateGslbCmd) {

        String algorithm = updateGslbCmd.getAlgorithm();
        String stickyMethod = updateGslbCmd.getStickyMethod();
        String description = updateGslbCmd.getDescription();

        long gslbRuleId = updateGslbCmd.getId();
        GlobalLoadBalancerRuleVO gslbRule = _gslbRuleDao.findById(gslbRuleId);
        if (gslbRule == null) {
            throw new InvalidParameterValueException("Invalid global load balancer rule id: " + gslbRuleId);
        }

        CallContext ctx = CallContext.current();
        Account caller = ctx.getCallingAccount();

        _accountMgr.checkAccess(caller, SecurityChecker.AccessType.OperateEntry, true, gslbRule);

        if (algorithm != null && !GlobalLoadBalancerRule.Algorithm.isValidAlgorithm(algorithm)) {
            throw new InvalidParameterValueException("Invalid Algorithm: " + algorithm);
        }

        if (stickyMethod != null && !GlobalLoadBalancerRule.Persistence.isValidPersistence(stickyMethod)) {
            throw new InvalidParameterValueException("Invalid persistence: " + stickyMethod);
        }

        if (algorithm != null) {
            gslbRule.setAlgorithm(algorithm);
        }
        if (stickyMethod != null) {
            gslbRule.setPersistence(stickyMethod);
        }
        if (description != null) {
            gslbRule.setDescription(description);
        }
        gslbRule.setState(GlobalLoadBalancerRule.State.Add);
        _gslbRuleDao.update(gslbRule.getId(), gslbRule);

        try {
            s_logger.debug("Updating global load balancer with id " + gslbRule.getUuid());

            // apply the gslb rule on to the back end gslb service providers on zones participating in gslb
            applyGlobalLoadBalancerRuleConfig(gslbRuleId, false);

            // on success set state to Active
            gslbRule.setState(GlobalLoadBalancerRule.State.Active);
            _gslbRuleDao.update(gslbRule.getId(), gslbRule);

            return gslbRule;
        } catch (ResourceUnavailableException e) {
            throw new CloudRuntimeException("Failed to configure gslb config due to " + e.getMessage());
        }
    }

    @Override
    public List<GlobalLoadBalancerRule> listGlobalLoadBalancerRule(ListGlobalLoadBalancerRuleCmd listGslbCmd) {

        CallContext ctx = CallContext.current();
        Account caller = ctx.getCallingAccount();

        Integer regionId = listGslbCmd.getRegionId();
        Long ruleId = listGslbCmd.getId();
        List<GlobalLoadBalancerRule> response = new ArrayList<GlobalLoadBalancerRule>();
        if (regionId == null && ruleId == null) {
            throw new InvalidParameterValueException("Invalid arguments. At least one of region id, " + "rule id must be specified");
        }

        if (regionId != null && ruleId != null) {
            throw new InvalidParameterValueException("Invalid arguments. Only one of region id, " + "rule id must be specified");
        }

        if (ruleId != null) {
            GlobalLoadBalancerRule gslbRule = _gslbRuleDao.findById(ruleId);
            if (gslbRule == null) {
                throw new InvalidParameterValueException("Invalid gslb rule id specified");
            }
            _accountMgr.checkAccess(caller, org.apache.cloudstack.acl.SecurityChecker.AccessType.UseEntry, false, gslbRule);

            response.add(gslbRule);
            return response;
        }

        if (regionId != null) {
            List<GlobalLoadBalancerRuleVO> gslbRules = _gslbRuleDao.listByAccount(caller.getAccountId());
            if (gslbRules != null) {
                response.addAll(gslbRules);
            }
            return response;
        }

        return null;
    }

    @Override
    public List<LoadBalancer> listSiteLoadBalancers(long gslbRuleId) {
        List<GlobalLoadBalancerLbRuleMapVO> gslbLbMapVos = _gslbLbMapDao.listByGslbRuleId(gslbRuleId);
        List<LoadBalancer> siteLoadBalancers = new ArrayList<LoadBalancer>();
        if (gslbLbMapVos != null) {
            for (GlobalLoadBalancerLbRuleMapVO gslbLbMapVo : gslbLbMapVos) {
                LoadBalancerVO loadBalancer = _lbDao.findById(gslbLbMapVo.getLoadBalancerId());
                siteLoadBalancers.add(loadBalancer);
            }
            return siteLoadBalancers;
        }
        return null;
    }

    private boolean applyGlobalLoadBalancerRuleConfig(long gslbRuleId, boolean revoke) throws ResourceUnavailableException {

        GlobalLoadBalancerRuleVO gslbRule = _gslbRuleDao.findById(gslbRuleId);
        assert (gslbRule != null);

        String lbMethod = gslbRule.getAlgorithm();
        String persistenceMethod = gslbRule.getPersistence();
        String serviceType = gslbRule.getServiceType();

        // each Gslb rule will have a FQDN, formed from the domain name associated with the gslb rule
        // and the deployment DNS name configured in global config parameter 'cloud.dns.name'
        String domainName = gslbRule.getGslbDomain();
        String providerDnsName = _globalConfigDao.getValue(Config.CloudDnsName.key());
        String gslbFqdn = domainName + "." + providerDnsName;

        GlobalLoadBalancerConfigCommand gslbConfigCmd = new GlobalLoadBalancerConfigCommand(gslbFqdn, lbMethod, persistenceMethod, serviceType, gslbRuleId, revoke);

        // list of the physical network participating in global load balancing
        List<Pair<Long, Long>> gslbSiteIds = new ArrayList<Pair<Long, Long>>();

        // map of the zone and info corresponding to the load balancer configured in the zone
        Map<Long, SiteLoadBalancerConfig> zoneSiteLoadbalancerMap = new HashMap<Long, SiteLoadBalancerConfig>();

        List<GlobalLoadBalancerLbRuleMapVO> gslbLbMapVos = _gslbLbMapDao.listByGslbRuleId(gslbRuleId);

        assert (gslbLbMapVos != null && !gslbLbMapVos.isEmpty());

        for (GlobalLoadBalancerLbRuleMapVO gslbLbMapVo : gslbLbMapVos) {

            // get the zone in which load balancer rule is deployed
            LoadBalancerVO loadBalancer = _lbDao.findById(gslbLbMapVo.getLoadBalancerId());
            Network network = _networkDao.findById(loadBalancer.getNetworkId());
            long dataCenterId = network.getDataCenterId();
            long physicalNetworkId = network.getPhysicalNetworkId();

            gslbSiteIds.add(new Pair<Long, Long>(dataCenterId, physicalNetworkId));

            IPAddressVO ip = _ipAddressDao.findById(loadBalancer.getSourceIpAddressId());
            SiteLoadBalancerConfig siteLb =
                new SiteLoadBalancerConfig(gslbLbMapVo.isRevoke(), serviceType, ip.getAddress().addr(), Integer.toString(loadBalancer.getDefaultPortStart()),
                    dataCenterId);

            siteLb.setGslbProviderPublicIp(lookupGslbServiceProvider().getZoneGslbProviderPublicIp(dataCenterId, physicalNetworkId));
            siteLb.setGslbProviderPrivateIp(lookupGslbServiceProvider().getZoneGslbProviderPrivateIp(dataCenterId, physicalNetworkId));
            siteLb.setWeight(gslbLbMapVo.getWeight());

            zoneSiteLoadbalancerMap.put(network.getDataCenterId(), siteLb);
        }

        // loop through all the zones, participating in GSLB, and send GSLB config command
        // to the corresponding GSLB service provider in that zone
        for (Pair<Long, Long> zoneId : gslbSiteIds) {

            List<SiteLoadBalancerConfig> slbs = new ArrayList<SiteLoadBalancerConfig>();
            // set site as 'local' for the site in that zone
            for (Pair<Long, Long> innerLoopZoneId : gslbSiteIds) {
                SiteLoadBalancerConfig siteLb = zoneSiteLoadbalancerMap.get(innerLoopZoneId.first());
                siteLb.setLocal(zoneId.first() == innerLoopZoneId.first());
                slbs.add(siteLb);
            }

            gslbConfigCmd.setSiteLoadBalancers(slbs);
            gslbConfigCmd.setForRevoke(revoke);

            // revoke GSLB configuration completely on the site GSLB provider for the sites that no longer
            // are participants of a GSLB rule
            SiteLoadBalancerConfig siteLb = zoneSiteLoadbalancerMap.get(zoneId.first());
            if (siteLb.forRevoke()) {
                gslbConfigCmd.setForRevoke(true);
            }

            try {
                lookupGslbServiceProvider().applyGlobalLoadBalancerRule(zoneId.first(), zoneId.second(), gslbConfigCmd);
            } catch (ResourceUnavailableException | NullPointerException e) {
                String msg = "Failed to configure GSLB rule in the zone " + zoneId.first() + " due to " + e.getMessage();
                s_logger.warn(msg);
                throw new CloudRuntimeException(msg);
            }
        }

        return true;
    }

    @Override
    public boolean revokeAllGslbRulesForAccount(com.cloud.user.Account caller, long accountId) throws com.cloud.exception.ResourceUnavailableException {
        List<GlobalLoadBalancerRuleVO> gslbRules = _gslbRuleDao.listByAccount(accountId);
        if (gslbRules != null && !gslbRules.isEmpty()) {
            for (GlobalLoadBalancerRule gslbRule : gslbRules) {
                revokeGslbRule(gslbRule.getId(), caller);
            }
        }
        s_logger.debug("Successfully cleaned up GSLB rules for account id=" + accountId);
        return true;
    }

    private boolean checkGslbServiceEnabledInZone(long zoneId, long physicalNetworkId) {

        GslbServiceProvider gslbProvider = lookupGslbServiceProvider();
        if (gslbProvider == null) {
            throw new CloudRuntimeException("No GSLB provider is available");
        }

        return gslbProvider.isServiceEnabledInZone(zoneId, physicalNetworkId);
    }

    protected GslbServiceProvider lookupGslbServiceProvider() {
        return _gslbProviders.size() == 0 ? null : _gslbProviders.get(0);
    }

    @Override
    public GlobalLoadBalancerRule findById(long gslbRuleId) {
        return _gslbRuleDao.findById(gslbRuleId);
    }
}