/*
 * 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.global.virtualdomain;

import com.fasterxml.jackson.databind.node.ObjectNode;
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.onosproject.net.config.basics.SubjectFactories;
import org.opencord.ce.api.services.virtualprovider.DomainVirtualDevice;
import org.opencord.ce.api.services.virtualprovider.EcordDeviceProviderService;
import org.onlab.packet.ChassisId;
import org.onosproject.cluster.ClusterService;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.CoreService;
import org.onosproject.mastership.MastershipAdminService;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.DefaultAnnotations;
import org.onosproject.net.Device;
import org.onosproject.net.DeviceId;
import org.onosproject.net.MastershipRole;
import org.onosproject.net.Port;
import org.onosproject.net.PortNumber;
import org.onosproject.net.SparseAnnotations;
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.onosproject.net.config.NetworkConfigService;
import org.onosproject.net.device.DefaultDeviceDescription;
import org.onosproject.net.device.DeviceDescription;
import org.onosproject.net.device.DeviceProvider;
import org.onosproject.net.device.DeviceProviderRegistry;
import org.onosproject.net.device.DeviceProviderService;
import org.onosproject.net.device.PortDescription;
import org.onosproject.net.domain.DomainId;
import org.onosproject.net.provider.ProviderId;
import org.opencord.ce.api.models.CarrierEthernetEnni;
import org.opencord.ce.api.models.CarrierEthernetGenericNi;
import org.opencord.ce.api.models.CarrierEthernetInni;
import org.opencord.ce.api.models.CarrierEthernetLogicalTerminationPoint;
import org.opencord.ce.api.models.CarrierEthernetNetworkInterface;
import org.opencord.ce.api.models.CarrierEthernetUni;
import org.opencord.ce.api.services.MetroOrchestrationService;
import org.opencord.ce.global.virtualdomain.config.EcordDriverConfig;
import org.opencord.ce.global.virtualdomain.config.XosEndPointConfig;
import org.slf4j.Logger;

import java.util.List;
import java.util.concurrent.ExecutorService;

import static java.util.concurrent.Executors.newFixedThreadPool;
import static org.onlab.util.Tools.groupedThreads;
import static org.onosproject.net.AnnotationKeys.DRIVER;
import static org.onosproject.net.AnnotationKeys.LATITUDE;
import static org.onosproject.net.AnnotationKeys.LONGITUDE;
import static org.slf4j.LoggerFactory.getLogger;
import static org.opencord.ce.api.models.CarrierEthernetNetworkInterface.Type;

/**
 * Exposes remote domain devices to the core.
 */
@Component(immediate = true)
@Service(value = EcordDeviceProviderService.class)
public class VirtualDomainDeviceProvider
        implements DeviceProvider, EcordDeviceProviderService {
    private final Logger log = getLogger(getClass());
    public static final String PROVIDER_NAME = "org.opencord.ce.global.vprovider";
    public static final ProviderId PROVIDER_ID = new ProviderId("domain", PROVIDER_NAME);
    private static final String UNKNOWN = "unknown";
    private static final String NO_LLDP = "no-lldp";
    private static final String DOMAIN_ID = "domainId";
    private static final String MEF_IF_TYPE = "mefIfType";

    private ApplicationId appId;

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

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected NetworkConfigService configService;

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

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected DeviceProviderRegistry deviceProviderRegistry;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected MastershipAdminService mastershipAdminService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected ClusterService clusterService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected MetroOrchestrationService metroOrchestrationService;

    protected DeviceProviderService deviceProviderService;

    private EcordDriverConfig ecordDriverConfig;
    private XoSHttpClient xoSHttpClient;

    private final NetworkConfigListener configListener = new InternalConfigListener();

    /*
    private final ConfigFactory<ApplicationId, EcordDriverConfig> diverConfigFactory =
            new ConfigFactory<ApplicationId, EcordDriverConfig>(APP_SUBJECT_FACTORY,
                    EcordDriverConfig.class, "driver") {
                @Override
                public EcordDriverConfig createConfig() {
                    return new EcordDriverConfig();
                }
            };
            */

    private final ConfigFactory<ApplicationId, XosEndPointConfig> xosEndpointConfigFactory =
            new ConfigFactory<ApplicationId, XosEndPointConfig>(SubjectFactories.APP_SUBJECT_FACTORY,
                    XosEndPointConfig.class, "xos") {
                @Override
                public XosEndPointConfig createConfig() {
                    return new XosEndPointConfig();
                }
            };

    private final ExecutorService eventExecutor =
            newFixedThreadPool(3,
                    groupedThreads("onos/ecord-sb-manager", "event-handler-%d"));

    @Activate
    public void activate() {
        appId = coreService.registerApplication(PROVIDER_NAME);
       // configRegistry.registerConfigFactory(diverConfigFactory);
        configRegistry.registerConfigFactory(xosEndpointConfigFactory);
        configService.addListener(configListener);
        deviceProviderService = deviceProviderRegistry.register(this);
        log.info("Started");
    }

    @Deactivate
    public void deactivate() {
        //configRegistry.unregisterConfigFactory(diverConfigFactory);
        configRegistry.unregisterConfigFactory(xosEndpointConfigFactory);
        configService.removeListener(configListener);
        deviceProviderRegistry.unregister(this);
        log.info("Stopped");
    }

    @Override
    public ProviderId id() {
        return PROVIDER_ID;
    }

    // ==== DeviceProvider ====
    @Override
    public void triggerProbe(DeviceId deviceId) {
        // TODO Auto-generated method stub
    }

    @Override
    public void roleChanged(DeviceId deviceId, MastershipRole newRole) {

    }

    @Override
    public boolean isReachable(DeviceId deviceId) {
        // TODO
        return true;
    }

    @Override
    public void changePortState(DeviceId deviceId, PortNumber portNumber,
                                boolean enable) {
        // TODO
    }

    // ===== EcordDeviceProviderService =====
    @Override
    public void connectRemoteDevice(DomainVirtualDevice domainDevice) {
        DomainId domainId = domainDevice.domainId();
        DeviceId deviceId = domainDevice.deviceId();
        advertiseDevice(deviceId, domainId);
       // advertiseDevicePorts(deviceId, domainDevice.ports());
    }

    @Override
    public void addOrUpdateRemotePorts(DomainId domainId, DeviceId deviceId,
                                       List<PortDescription> ports) {
        advertiseDevicePorts(deviceId, ports);
    }

    @Override
    public void addRemotePort(DomainId domainId, DeviceId deviceId, Port newPort) {
    }

    @Override
    public void updateRemotePortState(DomainId domainId, DeviceId deviceId, Port port) {
        // TODO
    }

    @Override
    public void disconnectRemoteDevice(DomainId domainId, DeviceId deviceId) {
        disconnectDevice(deviceId);
    }

    @Override
    public void removeRemotePort(DomainId domainId, DeviceId deviceId, PortNumber portNumber) {
        // TODO
    }

    /**
     * Notify the core system that a new domain device is on.
     * @param deviceId remote device identifier
     */
    private void advertiseDevice(DeviceId deviceId, DomainId domainId) {
        ChassisId chassisId = new ChassisId();
        log.info("Notifying ecord virtual device...");
        mastershipAdminService.setRole(clusterService.getLocalNode().id(), deviceId,
                MastershipRole.MASTER);

        // disable lldp for this virtual device and annotate it with the proper driver
       // String driverKey = ecordDriverConfig.manufacturer() + "-" + ecordDriverConfig.hwVersion() + "-" +
         //       ecordDriverConfig.swVersion();
        String driverKey = "onLab-1.0.0-1.0.0";
        SparseAnnotations annotations = DefaultAnnotations.builder()
                .set(NO_LLDP, "any")
                .set(DOMAIN_ID, domainId.id())
                .set(DRIVER, driverKey)
                .build();

        DeviceDescription deviceDescription = new DefaultDeviceDescription(
                deviceId.uri(),
                Device.Type.SWITCH,
                "onLab", "1.0.0",
                "1.0.0", UNKNOWN,
                chassisId,
                annotations);
        deviceProviderService.deviceConnected(deviceId, deviceDescription);
    }

    /**
     * Notify the core system of all ports of a domain device.
     * @param deviceId device identifier
     * @param portDescriptions description of ports
     */
    private void advertiseDevicePorts(DeviceId deviceId, List<PortDescription> portDescriptions) {
        log.info("Notifying ecord virtual ports...");
        deviceProviderService.updatePorts(deviceId, portDescriptions);
       // addGlobalMefLtp(deviceId, portDescriptions);
        notifyXoS(deviceId, portDescriptions);
    }

    private void disconnectDevice(DeviceId deviceId) {
        deviceProviderService.deviceDisconnected(deviceId);
    }

    private void notifyXoS(DeviceId deviceId, List<PortDescription> portDescriptions) {
        portDescriptions.forEach(port -> {
            if (port.annotations().keys().contains(MEF_IF_TYPE)) {
                Type type = Type.valueOf(
                        port.annotations().value(MEF_IF_TYPE));
                ConnectPoint cp = new ConnectPoint(deviceId, port.portNumber());
                switch (type) {
                    case UNI:
                        ObjectNode body = xoSHttpClient.mapper().createObjectNode();
                        body.put("tenant", port.annotations().value(DOMAIN_ID));
                        body.put("name", "UNI:" + cp.toString());
                        body.put("latlng", "[" + port.annotations().value(LATITUDE) + "," +
                                port.annotations().value(LONGITUDE) + "]");
                        body.put("cpe_id", cp.toString());
                        xoSHttpClient.restPut(body.toString(), cp.toString());
                        break;
                    case ENNI:

                        break;
                    case INNI:

                        break;
                    case GENERIC:

                        break;
                    default:
                        return;
                }
            }
        });
    }

    /**
     * Adds global {@link CarrierEthernetLogicalTerminationPoint}
     * to {@link MetroOrchestrationService}.
     *
     * @param deviceId subject device ID
     * @param portDescriptions list of notified ports
     */
    private void addGlobalMefLtp(DeviceId deviceId, List<PortDescription> portDescriptions) {
        portDescriptions.forEach(port -> {
            if (port.annotations().keys().contains(MEF_IF_TYPE)) {
                Type type = Type.valueOf(
                        port.annotations().value(MEF_IF_TYPE));
                CarrierEthernetNetworkInterface ni;
                ConnectPoint cp = new ConnectPoint(deviceId, port.portNumber());
                switch (type) {
                    case UNI:
                        ni = CarrierEthernetUni.builder()
                                .cp(cp)
                                .annotations(port.annotations())
                                .build();
                        break;
                    case ENNI:
                        ni = CarrierEthernetEnni.builder()
                                .cp(cp)
                                .build();
                        break;
                    case INNI:
                        ni = CarrierEthernetInni.builder()
                                .cp(cp)
                                .annotations(port.annotations())
                                .build();
                        break;
                    case GENERIC:
                        ni = new CarrierEthernetGenericNi(cp, null, port.annotations());
                        break;
                    default:
                            return;
                }
                metroOrchestrationService.addGlobalLtp(
                        new CarrierEthernetLogicalTerminationPoint(null, ni));
            }
        });
    }

    private void readXoSEndPointConfig() {
        xoSHttpClient =
                new XoSHttpClient(configRegistry.getConfig(appId, XosEndPointConfig.class).xos());
    }

    private void readDriverFromConfig() {
        ecordDriverConfig =
                configRegistry.getConfig(appId, EcordDriverConfig.class);
    }

    private class InternalConfigListener implements NetworkConfigListener {
        @Override
        public void event(NetworkConfigEvent event) {
            if (!event.configClass().equals(EcordDriverConfig.class) ||
                    !event.configClass().equals(XosEndPointConfig.class)) {
                return;
            }
            switch (event.type()) {
                case CONFIG_ADDED:
                    log.info("Network configuration added");
                    if (event.configClass().equals(EcordDriverConfig.class)) {
                        eventExecutor.execute(VirtualDomainDeviceProvider.this::readDriverFromConfig);
                    } else {
                        eventExecutor.execute(VirtualDomainDeviceProvider.this::readXoSEndPointConfig);
                    }

                    break;
                case CONFIG_UPDATED:
                    log.info("Network configuration updated");
                    if (event.configClass().equals(EcordDriverConfig.class)) {
                        eventExecutor.execute(VirtualDomainDeviceProvider.this::readDriverFromConfig);
                    } else {
                        eventExecutor.execute(VirtualDomainDeviceProvider.this::readXoSEndPointConfig);
                    }
                    break;
                default:
                    break;
            }
        }
    }
}
