/*
 * Copyright 2017-present Open Networking Foundation
 *
 * 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.fabric;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
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.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.onlab.packet.IpAddress;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.CoreService;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.DeviceId;
import org.onosproject.net.config.ConfigFactory;
import org.onosproject.net.config.NetworkConfigEvent;
import org.onosproject.net.config.NetworkConfigListener;
import org.onosproject.net.config.NetworkConfigRegistry;
import org.opencord.ce.api.models.CarrierEthernetForwardingConstruct;
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 javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import static org.onosproject.net.config.basics.SubjectFactories.APP_SUBJECT_FACTORY;
import static org.slf4j.LoggerFactory.getLogger;

/**
 * Manages a single switch CORD fabric for Carrier Ethernet services.
 *
 * Receives forwarding constructs from global orchestrator,
 * and generates VLAN cross connect configuration for the
 * ONOS fabric controller.
 *
 * No resources are allocated so only the node forwarding API is implemented.
 */
@Component(immediate = true)
@Service
public class CarrierEthernetFabricManager implements MetroNetworkVirtualNodeService {
    private static final Logger log = getLogger(CarrierEthernetFabricManager.class);
    private static final String APP_NAME = "org.opencord.ce.local.fabric";
    private static final String APPS = "apps";
    private static final String SEGMENT_ROUTING = "org.onosproject.segmentrouting";
    private static final String XCONNECT = "xconnect";
    private static final String VLAN = "vlan";
    private static final String PORTS = "ports";
    private static final String NAME = "name";

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

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

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected NetworkConfigRegistry cfgService;

    private final InternalConfigListener cfgListener = new InternalConfigListener();

    private final ConfigFactory<ApplicationId, CarrierEthernetFabricConfig> configFactory =
            new ConfigFactory<ApplicationId, CarrierEthernetFabricConfig>(APP_SUBJECT_FACTORY,
                    CarrierEthernetFabricConfig.class, "segmentrouting_ctl") {
                @Override
                public CarrierEthernetFabricConfig createConfig() {
                    return new CarrierEthernetFabricConfig();
                }
            };

    private ApplicationId appId;
    // TODO: use distributed maps via storage service
    private Set<CarrierEthernetForwardingConstruct> forwardingConstructs = new LinkedHashSet<>();
    private Map<EvcConnId, ConnectPoint> eePorts = new HashMap<>();
    private Map<EvcConnId, ConnectPoint> upstreamPorts = new HashMap<>();
    private IpAddress publicIp;
    private Integer port;
    private String username;
    private String password;
    private DeviceId deviceId;

    @Activate
    public void activate() {
        appId = coreService.registerApplication(APP_NAME);
        cfgService.addListener(cfgListener);
        cfgService.registerConfigFactory(configFactory);
        cfgListener.doUpdate(cfgService.getConfig(appId, CarrierEthernetFabricConfig.class));
        log.info("Started");
    }

    @Deactivate
    public void deactivate() {
        cfgService.removeListener(cfgListener);
        cfgService.unregisterConfigFactory(configFactory);
        log.info("Stopped");
    }

    @Override
    public void setNodeForwarding(CarrierEthernetForwardingConstruct fc,
                                  CarrierEthernetNetworkInterface srcNi,
                                  Set<CarrierEthernetNetworkInterface> dstNiSet) {
        addCrossconnect(fc, srcNi, dstNiSet);
        postToSegmentRouting(buildConfig());
    }

    @Override
    public void createBandwidthProfileResources(CarrierEthernetForwardingConstruct fc, CarrierEthernetUni uni) {
        // No resources are allocated on the fabric
        return;
    }

    @Override
    public void applyBandwidthProfileResources(CarrierEthernetForwardingConstruct fc, CarrierEthernetUni uni) {
        // No resources are allocated on the fabric
        return;
    }

    @Override
    public void removeBandwidthProfileResources(CarrierEthernetForwardingConstruct fc, CarrierEthernetUni uni) {
        // No resources are allocated on the fabric
        return;
    }

    @Override
    public void removeAllForwardingResources(EvcConnId fcId) {
        removeCrossconnect(fcId);
        postToSegmentRouting(buildConfig());
    }

    /**
     * Adds a fabric cross connect based on given Carrier Ethernet service.
     *
     * @param fc forwarding construct
     * @param srcNi source network interface
     * @param dstNiSet set of destination network interfaces
     */
    public void addCrossconnect(CarrierEthernetForwardingConstruct fc,
                                CarrierEthernetNetworkInterface srcNi,
                                Set<CarrierEthernetNetworkInterface> dstNiSet) {
        // Store fc and extract physical fabric ports
        Optional<ConnectPoint> eePort = bigSwitchService.connectPointFromVirtPort(srcNi.cp().port());
        // Assume only a single upstream port is used, so we select randomly from set
        CarrierEthernetNetworkInterface dstNi = dstNiSet.iterator().next();
        Optional<ConnectPoint> upstreamPort = (dstNi == null) ? Optional.empty() :
                bigSwitchService.connectPointFromVirtPort(dstNi.cp().port());
        if (!eePort.isPresent() || !upstreamPort.isPresent()) {
            log.error("Failed to install node forwarding, missing fabric ports: EE {} - upstream {}",
                    eePort, upstreamPort);
            return;
        } else {
            forwardingConstructs.add(fc);
            eePorts.put(fc.id(), eePort.get());
            upstreamPorts.put(fc.id(), upstreamPort.get());
        }
    }

    /**
     * Removes a fabric cross connect based on given forwarding construct.
     *
     * @param fcId forwarding construct id
     */
    public void removeCrossconnect(EvcConnId fcId) {
        for (Iterator<CarrierEthernetForwardingConstruct> i = forwardingConstructs.iterator(); i.hasNext();) {
            CarrierEthernetForwardingConstruct fc = i.next();
            if (fcId.equals(fc.id())) {
                forwardingConstructs.remove(fc);
                break;
            }
        }
        eePorts.remove(fcId);
        upstreamPorts.remove(fcId);
    }


    /**
     * All VLAN cross connects are rebuilt and pushed out since ONOS network config does not support updates.
     *
     * @return JSON with cross connect configuration for segment routing app
     */
    public JsonObject buildConfig() {
        JsonArray xconnects = new JsonArray();
        forwardingConstructs.stream()
                .map(fc -> json(fc))
                .forEach(fc -> xconnects.add(fc));

        JsonObject dpid = new JsonObject();
        dpid.add(deviceId.toString(), xconnects);

        JsonObject xconnect = new JsonObject();
        xconnect.add(XCONNECT, dpid);

        JsonObject appName = new JsonObject();
        appName.add(SEGMENT_ROUTING, xconnect);

        JsonObject config = new JsonObject();
        config.add(APPS, appName);

        return config;
    }

    /**
     * Execute REST POST to segment routing app with given VLAN cross connect config.
     *
     * @param json fabric VLAN cross connect configuration in json form
     */
    public void postToSegmentRouting(JsonObject json) {
        // Setup credentials
        HttpAuthenticationFeature feature = HttpAuthenticationFeature.basicBuilder()
                .nonPreemptive()
                .credentials(username, password)
                .build();
        ClientConfig cfg = new ClientConfig();
        cfg.register(feature);
        Client client = ClientBuilder.newClient(cfg);

        // Build URL and perform post
        WebTarget target = client.target("http://" + publicIp + ":" + port + "/onos/v1/network/configuration/");
        Response response = target.request().post(Entity.entity(json.toString(), MediaType.APPLICATION_JSON_TYPE));
        response.close();
    }

    /**
     * Build fabric config json from forwarding construct.
     *
     * Example VLAN cross connect configuration for fabric
     * "apps": {
     *    "org.onosproject.segmentrouting": {
     *       "xconnect": {
     *          "of:0000000000000001": [{
     *             "vlan": 10,
     *             "ports": [5, 73],
     *             "name": "OLT1"
     *          }]
     *       }
     *    }
     * }
     */
    private JsonObject json(CarrierEthernetForwardingConstruct fc) {
        JsonObject jo = new JsonObject();
        jo.addProperty(VLAN, fc.vlanId().toShort());

        // First port is EE -> fabric, second is fabric -> upstream / CO egress
        JsonArray ports = new JsonArray();
        // FIXME: need to be more careful of nulls here
        ports.add(eePorts.get(fc.id()).port().toLong());
        ports.add(upstreamPorts.get(fc.id()).port().toLong());
        jo.add(PORTS, ports);

        jo.addProperty(NAME, fc.id().id());

        return jo;
    }

    private class InternalConfigListener implements NetworkConfigListener {
        private void doUpdate(CarrierEthernetFabricConfig cfg) {
            if (cfg == null) {
                log.error("Fabric config for VLAN xconnect missing");
                return;
            }

            publicIp = cfg.publicIp();
            port = cfg.port();
            username = cfg.username();
            password = cfg.password();
            deviceId = cfg.deviceId();
            log.info("Reconfigured");
        }

        @Override
        public void event(NetworkConfigEvent event) {
            if ((event.type() == NetworkConfigEvent.Type.CONFIG_ADDED ||
                    event.type() == NetworkConfigEvent.Type.CONFIG_UPDATED) &&
                    event.configClass().equals(CarrierEthernetFabricConfig.class)) {

                if (event.config().isPresent()) {
                    doUpdate((CarrierEthernetFabricConfig) event.config().get());
                }
            }
        }
    }
}
