/*
 * Copyright 2017-present Open Networking Laboratory
 *
 * 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 org.opencord.ce.local.vee;

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.Service;
import org.onlab.packet.EthType;
import org.onlab.packet.Ethernet;
import org.onlab.packet.VlanId;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.CoreService;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.DeviceId;
import org.onosproject.net.Link;
import org.onosproject.net.Path;
import org.onosproject.net.PortNumber;
import org.onosproject.net.driver.Driver;
import org.onosproject.net.driver.DriverService;
import org.onosproject.net.flow.DefaultFlowRule;
import org.onosproject.net.flow.DefaultTrafficSelector;
import org.onosproject.net.flow.DefaultTrafficTreatment;
import org.onosproject.net.flow.FlowRule;
import org.onosproject.net.flow.FlowRuleService;
import org.onosproject.net.flow.TrafficSelector;
import org.onosproject.net.flow.TrafficTreatment;
import org.onosproject.net.flow.criteria.Criteria;
import org.onosproject.net.flow.criteria.Criterion;
import org.onosproject.net.flow.instructions.Instruction;
import org.onosproject.net.flow.instructions.Instructions;
import org.onosproject.net.flow.instructions.L2ModificationInstruction;
import org.onosproject.net.flowobjective.DefaultFilteringObjective;
import org.onosproject.net.flowobjective.DefaultForwardingObjective;
import org.onosproject.net.flowobjective.DefaultNextObjective;
import org.onosproject.net.flowobjective.FilteringObjective;
import org.onosproject.net.flowobjective.FlowObjectiveService;
import org.onosproject.net.flowobjective.ForwardingObjective;
import org.onosproject.net.flowobjective.NextObjective;
import org.onosproject.net.flowobjective.Objective;
import org.onosproject.net.link.LinkService;
import org.onosproject.net.meter.Band;
import org.onosproject.net.meter.DefaultBand;
import org.onosproject.net.meter.DefaultMeterRequest;
import org.onosproject.net.meter.Meter;
import org.onosproject.net.meter.MeterId;
import org.onosproject.net.meter.MeterRequest;
import org.onosproject.net.meter.MeterService;
import org.onosproject.net.topology.PathService;
import org.opencord.ce.api.models.CarrierEthernetEnni;
import org.opencord.ce.api.models.CarrierEthernetForwardingConstruct;
import org.opencord.ce.api.models.CarrierEthernetGenericNi;
import org.opencord.ce.api.models.CarrierEthernetInni;
import org.opencord.ce.api.models.CarrierEthernetNetworkInterface;
import org.opencord.ce.api.models.CarrierEthernetUni;
import org.opencord.ce.api.models.EvcConnId;
import org.opencord.ce.api.services.MetroNetworkVirtualNodeService;

import org.opencord.ce.local.bigswitch.BigSwitchService;
import org.slf4j.Logger;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import static org.slf4j.LoggerFactory.getLogger;
import static org.opencord.ce.api.models.CarrierEthernetNetworkInterface.Type;
import static org.onosproject.net.DefaultEdgeLink.createEdgeLink;

/**
 * Class used to control Ethernet Edge nodes according to the OpenFlow (1.3 and above) protocol.
 */
@Component(immediate = true)
@Service(value = MetroNetworkVirtualNodeService.class)
public class VeeManager implements MetroNetworkVirtualNodeService {
    private static final int PRIORITY = 50000;
    public static final String APP_NAME = "org.opencord.ce.local.vee";

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected CoreService coreService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected FlowRuleService flowRuleService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected MeterService meterService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected FlowObjectiveService flowObjectiveService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected LinkService linkService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected BigSwitchService bigSwitchService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected PathService pathService;

    // FIXME slightly better way to detect OF-DPA issues
    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected DriverService drivers;

    private final Logger log = getLogger(getClass());

    private ApplicationId appId;

    // FIXME: We don't need to store the submitted meters as we can get them from the service
    private Map<EvcConnId, Set<Pair<DeviceId, MeterId>>> fcMeterMap =
            new ConcurrentHashMap<>();
    // Store submitted flow objects as the service does not allow query operations
    private final Map<EvcConnId, LinkedList<Pair<DeviceId, Objective>>> flowObjectiveMap =
            new ConcurrentHashMap<>();

    private final Map<EvcConnId, DeviceId> eeDeviceMap = new ConcurrentHashMap<>();

    @Activate
    protected void activate() {
        appId = coreService.registerApplication(APP_NAME);

        // TODO: start packet intercept for untagged traffic?
        log.info("Started");

       // testCentec();
    }

    @Deactivate
    protected void deactivate() {

        log.info("Stopped");
    }


    @Override
    public void setNodeForwarding(CarrierEthernetForwardingConstruct fc, CarrierEthernetNetworkInterface ingressNi,
                                  Set<CarrierEthernetNetworkInterface> egressNiSet) {

        log.info("DEBUG: setForwarding method called..");
        if (ingressNi == null || egressNiSet.isEmpty()) {
            log.error("There needs to be at least one ingress and one egress NI to set forwarding.");
            return;
        }

        flowObjectiveMap.putIfAbsent(fc.id(), new LinkedList<>());

        CarrierEthernetNetworkInterface realIngressNi;
        Optional<ConnectPoint> optCp =
                bigSwitchService.connectPointFromVirtPort(ingressNi.cp().port());
        if (optCp.isPresent()) {
            realIngressNi = buildLocalNi(optCp.get(), ingressNi);
        } else {
            log.warn("Virtual ingress interface does not map to a local connect point");
            return;
        }

        // real <-> virtual
        final Map<ConnectPoint, CarrierEthernetNetworkInterface> realEgressNi = new HashMap<>();
        egressNiSet.forEach(egressNi -> {
            Optional<ConnectPoint> opt =
                    bigSwitchService.connectPointFromVirtPort(egressNi.cp().port());
            opt.ifPresent(connectPoint -> realEgressNi.put(connectPoint, egressNi));
        });

        // it is necessary to identify the number of egress devices in order to build the
        // flow rules
        Set<DeviceId> egressDevices = new HashSet<>();
        realEgressNi.keySet().forEach(cp -> egressDevices.add(cp.deviceId()));

        //for how it is made the CE app, egressNiSet contains only one item, but...
        egressDevices.forEach(deviceId -> {
            // group the egress NI per device
            Set<CarrierEthernetNetworkInterface> perDeviceEgressNiSet = new HashSet<>();
            realEgressNi.keySet().forEach(cp -> {
                if (cp.deviceId().equals(deviceId)) {
                    perDeviceEgressNiSet.add(buildLocalNi(cp, realEgressNi.get(cp)));
                }
            });
            // if the egress device ID is equal to the ingress --> install rules
            if (deviceId.equals(realIngressNi.cp().deviceId())) {
                createFlowObjectives(fc, realIngressNi,
                        perDeviceEgressNiSet);
            } else {
                Set<Path> paths = pathService.getPaths(realIngressNi.cp().deviceId(),
                        deviceId);
                Path path;
                // TODO: Select path in more sophisticated way and return null if any of the constraints cannot be met
                path = paths.iterator().hasNext() ? paths.iterator().next() : null;
                if (path == null) {
                    return;
                }
                // for each NI, add edge links ()
                perDeviceEgressNiSet.forEach(egressNi -> {
                    List<Link> links = new ArrayList<>();
                    links.add(createEdgeLink(realIngressNi.cp(), true));
                    links.addAll(path.links());
                    links.add(createEdgeLink(egressNi.cp(), false));

                    HashMap<CarrierEthernetNetworkInterface, HashSet<CarrierEthernetNetworkInterface>>
                            ingressEgressNiMap = new HashMap<>();
                    populateIngressEgressNiMap(realIngressNi, egressNi, links, ingressEgressNiMap);
                    // Establish connectivity using the ingressEgressNiMap
                    ingressEgressNiMap.keySet().forEach(srcNi ->
                        createFlowObjectives(fc, srcNi, ingressEgressNiMap.get(srcNi)));
                });
            }
        });
    }

    private void populateIngressEgressNiMap(CarrierEthernetNetworkInterface srcNi,
                                            CarrierEthernetNetworkInterface dstNi,
                                            List<Link> linkList,
                                            HashMap<CarrierEthernetNetworkInterface,
                                                    HashSet<CarrierEthernetNetworkInterface>> ingressEgressNiMap
    ) {
        // FIXME: Fix the method - avoid generating GENERIC NIs if not needed
        // Add the src and destination NIs as well as the associated Generic NIs
        ingressEgressNiMap.putIfAbsent(srcNi, new HashSet<>());
        // Add last hop entry only if srcNi, dstNi aren't on same device (in which case srcNi, ingressNi would coincide)
        if (!srcNi.cp().deviceId().equals(dstNi.cp().deviceId())) {
            // If srcNi, dstNi are not on the same device, create mappings to/from new GENERIC NIs
            ingressEgressNiMap.get(srcNi).add(new CarrierEthernetGenericNi(linkList.get(1).src(), null));
            CarrierEthernetGenericNi ingressNi =
                    new CarrierEthernetGenericNi(linkList.get(linkList.size() - 2).dst(), null);
            ingressEgressNiMap.putIfAbsent(ingressNi, new HashSet<>());
            ingressEgressNiMap.get(ingressNi).add(dstNi);
        } else {
            // If srcNi, dstNi are on the same device, this is the only mapping that will be created
            ingressEgressNiMap.get(srcNi).add(dstNi);
        }

        // Go through the links and create/add the intermediate NIs
        for (int i = 1; i < linkList.size() - 2; i++) {
            CarrierEthernetGenericNi ingressNi = new CarrierEthernetGenericNi(linkList.get(i).dst(), null);
            ingressEgressNiMap.putIfAbsent(ingressNi, new HashSet<>());
            ingressEgressNiMap.get(ingressNi).add(new CarrierEthernetGenericNi(linkList.get(i + 1).src(), null));
        }
    }

    private CarrierEthernetNetworkInterface buildLocalNi(ConnectPoint localCp, CarrierEthernetNetworkInterface
                                                         virtualNi) {
        CarrierEthernetNetworkInterface ni;
        switch (virtualNi.type()) {
            case UNI:
                CarrierEthernetUni tmpUni = (CarrierEthernetUni) virtualNi;
                ni = CarrierEthernetUni.builder()
                        .cp(localCp)
                        .role(tmpUni.role())
                        .ceVlanId(tmpUni.ceVlanId())
                        .bwp(tmpUni.bwp())
                        .build();
                break;
            case ENNI:
                CarrierEthernetEnni tmpEnni = (CarrierEthernetEnni) virtualNi;
                ni = CarrierEthernetEnni.builder()
                        .cp(localCp)
                        .role(tmpEnni.role())
                        .sVlanId(tmpEnni.sVlanId())
                        .build();
                break;
            case INNI:
                CarrierEthernetInni tmpInni = (CarrierEthernetInni) virtualNi;
                ni = CarrierEthernetInni.builder()
                        .cp(localCp)
                        .role(tmpInni.role())
                        .sVlanId(tmpInni.sVlanId())
                        .build();
                break;
            case GENERIC:
                ni = new CarrierEthernetGenericNi(localCp, null);
                break;
            default:
                return null;
        }
        return ni;

    }

    /**
     * Creates and submits FlowObjectives into the device vesting the role of Ethernet Edge.
     *
     * @param fc the FC representation
     * @param ingressNi the ingress network interface
     * @param  egressNiSet the set of egress NIs
     */
    private void createFlowObjectives(CarrierEthernetForwardingConstruct fc, CarrierEthernetNetworkInterface ingressNi,
                                      Set<CarrierEthernetNetworkInterface> egressNiSet) {
        DeviceId deviceId = ingressNi.cp().deviceId();
        PortNumber portNumber = ingressNi.cp().port();

        /////////////////////////////////////////
        // Prepare and submit filtering objective
        /////////////////////////////////////////

        FilteringObjective.Builder filterObjBuilder = DefaultFilteringObjective.builder()
                .permit().fromApp(appId)
                .withPriority(PRIORITY)
                .withKey(Criteria.matchInPort(portNumber));

        TrafficTreatment.Builder filterTreatmentBuilder = DefaultTrafficTreatment.builder();
        // In general, nodes would match on the VLAN tag assigned to the EVC/FC
        Criterion filterVlanIdCriterion = Criteria.matchVlanId(fc.vlanId());

        if ((ingressNi.type().equals(CarrierEthernetNetworkInterface.Type.INNI))
                || (ingressNi.type().equals(CarrierEthernetNetworkInterface.Type.ENNI))) {
            // TODO: Check TPID? Also: Is is possible to receive untagged pkts at an INNI/ENNI?
            // Source node of an FC should match on S-TAG if it's an INNI/ENNI
            filterVlanIdCriterion = Criteria.matchVlanId(ingressNi.sVlanId());
            // Translate S-TAG to the one used in the current FC
            filterTreatmentBuilder.setVlanId(fc.vlanId());
        } else if (ingressNi.type().equals(CarrierEthernetNetworkInterface.Type.UNI)) {
            // Source node of an FC should match on CE-VLAN ID (if present) if it's a UNI
            filterVlanIdCriterion = Criteria.matchVlanId(ingressNi.ceVlanId());
            // Obtain related Meter (if it exists) and add it in the treatment in case it may be used
            if (fcMeterMap.get(fc.id()) != null && !fcMeterMap.get(fc.id()).isEmpty()) {
                fcMeterMap.get(fc.id()).forEach(devMeterPair -> {
                    if (devMeterPair.getLeft().equals(deviceId)) {
                        filterTreatmentBuilder.meter(devMeterPair.getRight());
                    }
                });
            }
            // If a CE-VLAN-ID exists on the incoming packet then push an S-TAG of current FC on top
            // otherwise push it on as a C-tag
            if (ingressNi.ceVlanId() != null && ingressNi.ceVlanId() != VlanId.NONE) {
                filterTreatmentBuilder.pushVlan(EthType.EtherType.QINQ.ethType()).setVlanId(fc.vlanId());
            } else {
                filterTreatmentBuilder.pushVlan().setVlanId(fc.vlanId());
            }
        }

        filterObjBuilder.addCondition(filterVlanIdCriterion);



        // Do not add meta if there are no instructions (i.e. if not first)
        if (!(ingressNi.type().equals(Type.GENERIC))) {
            filterObjBuilder.withMeta(filterTreatmentBuilder.build());
        }

        flowObjectiveService.filter(deviceId, filterObjBuilder.add());
        flowObjectiveMap.get(fc.id()).addFirst(Pair.of(deviceId, filterObjBuilder.add()));

        ////////////////////////////////////////////////////
        // Prepare and submit next and forwarding objectives
        ////////////////////////////////////////////////////

        TrafficSelector.Builder fwdSelectorBuilder = DefaultTrafficSelector.builder()
                .matchVlanId(fc.vlanId())
                .matchInPort(portNumber);

        if (isOfDpa(deviceId)) {
            // workaround for OF-DPA
            fwdSelectorBuilder.matchEthType(Ethernet.TYPE_IPV4);
        }

        TrafficSelector fwdSelector = fwdSelectorBuilder.build();

        Integer nextId = flowObjectiveService.allocateNextId();

        NextObjective.Type nextType = egressNiSet.size() == 1 ?
                NextObjective.Type.SIMPLE : NextObjective.Type.BROADCAST;

        // Setting higher priority to fwd/next objectives to bypass filter in case of match conflict in OVS switches
        NextObjective.Builder nextObjectiveBuider = DefaultNextObjective.builder()
                .fromApp(appId)
                .makePermanent()
                .withType(nextType)
                .withPriority(PRIORITY + 1)
                .withMeta(fwdSelector)
                .withId(nextId);

        egressNiSet.forEach(egressNi -> {
            // TODO: Check if ingressNi and egressNi are on the same device?
            TrafficTreatment.Builder nextTreatmentBuilder = DefaultTrafficTreatment.builder();
            // If last NI in FC is not UNI,
            // keep the existing S-TAG - it will be translated at the entrance of the next FC
            if (egressNi.type().equals(CarrierEthernetNetworkInterface.Type.UNI)) {
                nextTreatmentBuilder.popVlan();
            }
            Instruction outInstruction = Instructions.createOutput(egressNi.cp().port());
            nextTreatmentBuilder.add(outInstruction);
            nextObjectiveBuider.addTreatment(nextTreatmentBuilder.build());
        });

        NextObjective nextObjective = nextObjectiveBuider.add();

        // Setting higher priority to fwd/next objectives to bypass filter in case of match conflict in OVS switches
        ForwardingObjective forwardingObjective = DefaultForwardingObjective.builder()
                .fromApp(appId)
                .makePermanent()
                .withFlag(ForwardingObjective.Flag.VERSATILE)
                .withPriority(PRIORITY + 1)
                .withSelector(fwdSelector)
                .nextStep(nextId)
                .add();

        flowObjectiveService.next(ingressNi.cp().deviceId(), nextObjective);
        // Add all NextObjectives at the end of the list so that they will be removed last
        flowObjectiveMap.get(fc.id()).addLast(Pair.of(ingressNi.cp().deviceId(), nextObjective));

        flowObjectiveService.forward(ingressNi.cp().deviceId(), forwardingObjective);
        flowObjectiveMap.get(fc.id()).addFirst(Pair.of(ingressNi.cp().deviceId(), forwardingObjective));
    }

    @Override
    public void createBandwidthProfileResources(CarrierEthernetForwardingConstruct fc, CarrierEthernetUni uni) {
        log.info("Creating BW profile...{}", uni.toString());
        // Create meters and add them to global MeterId map
        Set<Pair<DeviceId, MeterId>> meters;

        DeviceId deviceId = getEEDevice(fc, uni);

        if (fcMeterMap.containsKey(fc.id())) {
            meters = fcMeterMap.get(fc.id());
        } else {
            meters = new HashSet<>();
            if (deviceId == null) {
                // no meters
                return;
            }
        }
        meters.addAll(submitMeters(fc, uni, deviceId));
        fcMeterMap.put(fc.id(), meters);
    }

    private boolean isOfDpa(DeviceId deviceId) {
        Driver driver = drivers.getDriver(deviceId);
        return driver != null &&
                driver.swVersion().contains("OF-DPA");
    }

    // Maybe we still need this method to later modify the QoS profile of an FC
    @Override
    public void applyBandwidthProfileResources(CarrierEthernetForwardingConstruct fc, CarrierEthernetUni uni) {
        log.info("Applying BW profile...");

        DeviceId deviceId = eeDeviceMap.get(fc.id());
        if (deviceId == null) {
            log.warn("Trying to apply bandwidth profile rules" +
                    "to not existing device/meters map");
            return;
        }

        // Do not apply meters to NETCONF-controlled switches here since they should have been applied in the pipeline
        // FIXME: Is there a better way to check this?
        if (deviceId.uri().getScheme().equals("netconf")) {
            return;
        }

        // Do not apply meters to OFDPA 2.0 switches since they are not currently supported
        if (isOfDpa(deviceId)) {
            return;
        }

        // Get installed flows with the same appId/deviceId with IN_PORT = UNI port which push the FC vlanId
        List<FlowRule> flowRuleList =
                StreamSupport.stream(flowRuleService.getFlowEntries(deviceId).spliterator(), false)
                        .filter(flowRule -> flowRule.appId() == appId.id())
                        .collect(Collectors.toList());
                                //&& getPushedVlanFromTreatment(flowRule.treatment()).equals(fc.vlanId()))


        // Apply meters to flows
        for (FlowRule flowRule : flowRuleList) {
            // Need to add to the flow the meters associated with the same device
            Set<Pair<DeviceId, MeterId>> deviceMeterIdSet = new HashSet<>();

            if (fcMeterMap.get(fc.id()) != null && !fcMeterMap.get(fc.id()).isEmpty()) {
                fcMeterMap.get(fc.id()).forEach(devMeterPair -> {
                    if (devMeterPair.getLeft().equals(flowRule.deviceId())) {
                        deviceMeterIdSet.add(devMeterPair);
                    }
                });
            }
            // Modify and submit flow rule only if there are meters to add
            if (!deviceMeterIdSet.isEmpty()) {
                log.info("Applying metered flow rules...");
                FlowRule newFlowRule = addMetersToFlowRule(flowRule, deviceMeterIdSet);
                flowRuleService.applyFlowRules(newFlowRule);
            }
        }
    }

    private VlanId getPushedVlanFromTreatment(TrafficTreatment treatment) {
        boolean pushVlan = false;
        VlanId pushedVlan = null;
        for (Instruction instruction : treatment.allInstructions()) {
            if (instruction.type().equals(Instruction.Type.L2MODIFICATION)) {
                L2ModificationInstruction l2ModInstr = (L2ModificationInstruction) instruction;
                if (l2ModInstr.subtype().equals(L2ModificationInstruction.L2SubType.VLAN_PUSH)) {
                    pushVlan = true;
                } else if (l2ModInstr.subtype().equals(L2ModificationInstruction.L2SubType.VLAN_ID) && pushVlan) {
                    pushedVlan = ((L2ModificationInstruction.ModVlanIdInstruction) instruction).vlanId();
                }
            }
        }
        return pushedVlan != null ? pushedVlan : VlanId.NONE;
    }


    /**
     * Creates and submits a meter with the required bands for a UNI.
     *
     * @param uni the UNI descriptor
     * @return set of meter ids of the meters created
     */
    private Set<Pair<DeviceId, MeterId>> submitMeters(CarrierEthernetForwardingConstruct fc,
                                                      CarrierEthernetUni uni, DeviceId deviceId) {
        Set<Pair<DeviceId, MeterId>> meters = new HashSet<>();

        uni.bwps().forEach(bwp -> {
            log.info("bwp: {}", bwp.toString());

            // KB_PER_SECOND
            long longCir = (long) (bwp.cir().bps() / 8000);
            long longEir = (long) (bwp.eir().bps() / 8000);
            log.info("longCir: {}", longCir);
            log.info("longEir: {}", longEir);

            MeterRequest.Builder meterRequestBuilder;
            Meter meter;
            Band.Builder bandBuilder;

            Set<Band> bandSet = new HashSet<>();

            // If EIR is zero do not create the REMARK meter
            /* === Centec v350 supports only DROP type! ===
            if (longEir != 0) {
                log.info("Enter here..");
                // Mark frames that exceed CIR as Best Effort
                bandBuilder = DefaultBand.builder()
                        .ofType(Band.Type.REMARK)
                        .withRate(longCir)
                        .dropPrecedence((short) 0);

                if (bwp.cbs() != 0) {
                    bandBuilder.burstSize(bwp.cbs());
                }

                bandSet.add(bandBuilder.build());
            }
            */

            // If CIR is zero do not create the DROP meter
            if (longCir != 0) {
                // Drop all frames that exceed CIR + EIR
                bandBuilder = DefaultBand.builder()
                        .ofType(Band.Type.DROP)
                        .withRate(longCir + longEir);

                if (bwp.cbs() != 0 || bwp.ebs() != 0) {
                    // FIXME: Use CBS and EBS correctly according to MEF specs
                    bandBuilder.burstSize(bwp.cbs() + bwp.ebs());
                }

                bandSet.add(bandBuilder.build());
            }

            // Create meter only if at least one band was created
            if (!bandSet.isEmpty()) {
                meterRequestBuilder = DefaultMeterRequest.builder()
                        .forDevice(deviceId)
                        .fromApp(appId)
                        .withUnit(Meter.Unit.KB_PER_SEC)
                        .withBands(bandSet);

                if (bwp.cbs() != 0 || bwp.ebs() != 0) {
                    meterRequestBuilder.burst();
                }

                // Submit meter request and store
                meter = meterService.submit(meterRequestBuilder.add());
                // FIXME: use correct device id
                log.info("Adding meter...{}", meter.toString());

                meters.add(new ImmutablePair<>(deviceId, meter.id()));
            }
        });

        return meters;
    }

    private FlowRule addMetersToFlowRule(FlowRule flowRule,  Set<Pair<DeviceId, MeterId>> deviceMeterIdSet) {

        TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment
                .builder(flowRule.treatment());

        deviceMeterIdSet.forEach(deviceMeterPair ->
            tBuilder.meter(deviceMeterPair.getRight()));

        return createFlowRule(flowRule.deviceId(), flowRule.priority(),
                flowRule.selector(), tBuilder.build(), flowRule.tableId());
    }

    @Override
    public void removeBandwidthProfileResources(CarrierEthernetForwardingConstruct fc, CarrierEthernetUni uni) {
        removeMeters(fc, uni);
    }

    /**
     * Removes the meters associated with a specific UNI of an FC.
     *
     * @param fc the forwarding construct
     * @param uni the UNI descriptor
     * */
    private void removeMeters(CarrierEthernetForwardingConstruct fc, CarrierEthernetUni uni) {

        if (!fcMeterMap.containsKey(fc.id())) {
            return;
        }

        Set<Pair<DeviceId, MeterId>> ids = fcMeterMap.get(fc.id());

        // Rebuild meter request based on existing meter, withdraw the request, and remove it from our internal storage
        ids.stream()
                .map(id -> meterService.getMeter(id.getLeft(), id.getRight()))
                .filter(Objects::nonNull)
                .forEach(meter -> {
                    MeterRequest.Builder request = DefaultMeterRequest.builder()
                            .fromApp(meter.appId())
                            .forDevice(meter.deviceId())
                            .withUnit(meter.unit())
                            .withBands(meter.bands());
                    if (uni.bwp().cbs() != 0 || uni.bwp().ebs() != 0) {
                        request.burst();
                    }
                    meterService.withdraw(request.remove(), meter.id());

                    ids.remove(new ImmutablePair<>(meter.deviceId(), meter.id()));
                });
    }

    /**
     * Removes all flow objectives installed by the application which are associated with a specific FC.
     *
     * @param fcId ForwardingConcrtuct object
     */
    @Override
    public void removeAllForwardingResources(EvcConnId fcId) {
        // Note: A Flow Rule cannot be shared by multiple FCs due to different VLAN or CE-VLAN ID match.
        List<Pair<DeviceId, Objective>> flowObjectives = flowObjectiveMap.remove(fcId);
        // NextObjectives will be removed after all other Objectives
        // TODO: filter also on elements of pair
        flowObjectives.stream()
                .filter(Objects::nonNull)
                .forEach(pair ->
                        flowObjectiveService.apply(pair.getLeft(), pair.getRight().copy().remove()));
    }

    private FlowRule createFlowRule(DeviceId deviceId, int priority,
                                    TrafficSelector selector, TrafficTreatment treatment, int tableId) {
        return DefaultFlowRule.builder()
                .fromApp(appId)
                .forDevice(deviceId)
                .makePermanent()
                .withPriority(priority)
                .withSelector(selector)
                .withTreatment(treatment)
                .forTable(tableId)
                .build();
    }

    /**
     * !! Caution !! This method is strictly tied to the expected topology
     * view of this {@link MetroNetworkVirtualNodeService} implementation.
     * The UNI is expected to be in the CPE and meters implementation in the EE.
     * The CPE node shall be connected only
     * with the EE device (exception are multi path home gateway).
     *
     * @param fc forwarding construct
     * @param uni User to Network Interface
     * @return the EE device ID
     */
    private DeviceId getEEDevice(CarrierEthernetForwardingConstruct fc, CarrierEthernetUni uni)
            throws IllegalStateException {
        CarrierEthernetNetworkInterface realUni;
        Optional<ConnectPoint> optCp =
                bigSwitchService.connectPointFromVirtPort(uni.cp().port());
        if (optCp.isPresent()) {
            realUni = buildLocalNi(optCp.get(), uni);
        } else {
            log.info("Virtual ingress interface does not map to a local connect point");
            throw new IllegalStateException("Virtual UNI interface does not map to a local connect point");
        }
        if (eeDeviceMap.get(fc.id()) != null) {
            return eeDeviceMap.get(fc.id());
        }
        // find the (only) egress link of the UNI device
        Optional<Link> optLink = linkService.getDeviceEgressLinks(realUni.cp().deviceId())
                .stream().findFirst();
        if (optLink.isPresent()) {

            // the link destination should be our target EE device
            DeviceId deviceId = optLink.get().dst().deviceId();
            log.info("Adding EE device {} in memory...", deviceId.toString());
            eeDeviceMap.put(fc.id(), deviceId);
            return deviceId;
        } else {
            log.info("No EE device found for meters...uni: {}", realUni.cp().toString());
            // in this case there is no EE upstream and so no meter will be installed
            return null;
        }
    }
}