/*
 * Copyright 2015-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.cordvtn.api;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.onlab.osgi.DefaultServiceDirectory;
import org.onlab.packet.IpAddress;
import org.onlab.packet.MacAddress;
import org.onlab.packet.TpPort;
import org.onosproject.cluster.ClusterService;
import org.onosproject.core.ApplicationId;
import org.onosproject.net.DeviceId;
import org.onosproject.net.behaviour.ControllerInfo;
import org.onosproject.net.config.Config;
import org.onosproject.net.config.InvalidFieldException;
import org.opencord.cordvtn.api.net.CidrAddr;
import org.opencord.cordvtn.api.node.CordVtnNode;
import org.opencord.cordvtn.api.node.SshAccessInfo;
import org.opencord.cordvtn.impl.DefaultCordVtnNode;
import org.slf4j.Logger;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static org.onosproject.net.config.Config.FieldPresence.MANDATORY;
import static org.onosproject.net.config.Config.FieldPresence.OPTIONAL;
import static org.opencord.cordvtn.api.Constants.DEFAULT_OF_PORT;
import static org.opencord.cordvtn.api.Constants.DEFAULT_OF_PROTOCOL;
import static org.slf4j.LoggerFactory.getLogger;

/**
 * Configuration object for CordVtn service.
 */
public class CordVtnConfig extends Config<ApplicationId> {

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

    @Deprecated
    private static final String PRIVATE_GATEWAY_MAC = "privateGatewayMac";
    private static final String PUBLIC_GATEWAYS = "publicGateways";
    private static final String GATEWAY_IP = "gatewayIp";
    private static final String GATEWAY_MAC = "gatewayMac";
    private static final String LOCAL_MANAGEMENT_IP = "localManagementIp";
    private static final String OVSDB_PORT = "ovsdbPort";

    private static final String CORDVTN_NODES = "nodes";
    private static final String HOSTNAME = "hostname";
    private static final String HOST_MANAGEMENT_IP = "hostManagementIp";
    private static final String HOST_MANAGEMENT_IFACE = "hostManagementIface";
    private static final String DATA_IP = "dataPlaneIp";
    private static final String DATA_IFACE = "dataPlaneIntf";
    private static final String INTEGRATION_BRIDGE_ID = "bridgeId";

    private static final String SSH = "ssh";
    private static final String SSH_PORT = "sshPort";
    private static final String SSH_USER = "sshUser";
    private static final String SSH_KEY_FILE = "sshKeyFile";

    @Deprecated
    private static final String OPENSTACK = "openstack";
    @Deprecated
    private static final String XOS = "xos";

    private static final String CONTROLLERS = "controllers";
    private static final int INDEX_IP = 0;
    private static final int INDEX_PORT = 1;

    private final ClusterService clusterService =
            DefaultServiceDirectory.getService(ClusterService.class);

    @Override
    public boolean isValid() {
        // check only allowed fields are present
        boolean result = hasOnlyFields(
                PRIVATE_GATEWAY_MAC,
                PUBLIC_GATEWAYS,
                LOCAL_MANAGEMENT_IP,
                OVSDB_PORT,
                SSH,
                OPENSTACK,
                XOS,
                CORDVTN_NODES,
                CONTROLLERS);

        if (object.get(CORDVTN_NODES) == null || object.get(CORDVTN_NODES).size() < 1) {
            final String msg = "No node is present";
            throw new IllegalArgumentException(msg);
        }

        // check all mandatory fields are present and valid
        result = result && isIpPrefix(LOCAL_MANAGEMENT_IP, MANDATORY);

        for (JsonNode node : object.get(CORDVTN_NODES)) {
            ObjectNode vtnNode = (ObjectNode) node;
            result = result && hasFields(
                    vtnNode,
                    HOSTNAME,
                    HOST_MANAGEMENT_IP,
                    DATA_IP,
                    DATA_IFACE,
                    INTEGRATION_BRIDGE_ID);
            result = result && isIpPrefix(vtnNode, HOST_MANAGEMENT_IP, MANDATORY);
            result = result && isIpPrefix(vtnNode, DATA_IP, MANDATORY);

            CidrAddr localMgmt = CidrAddr.valueOf(get(LOCAL_MANAGEMENT_IP, ""));
            CidrAddr hostsMgmt = CidrAddr.valueOf(getConfig(vtnNode, HOST_MANAGEMENT_IP));
            if (hostsMgmt.prefix().contains(localMgmt.prefix()) ||
                    localMgmt.prefix().contains(hostsMgmt.prefix())) {
                final String msg = "Host and local management network IP conflict";
                throw new IllegalArgumentException(msg);
            }
        }

        result = result && hasFields(
                (ObjectNode) object.get(SSH),
                SSH_PORT,
                SSH_USER,
                SSH_KEY_FILE);
        result = result && isTpPort(
                (ObjectNode) object.get(SSH),
                SSH_PORT,
                MANDATORY);

        // check all optional fields are valid
        result = result && isTpPort(OVSDB_PORT, OPTIONAL);

        if (object.get(PUBLIC_GATEWAYS) != null && object.get(PUBLIC_GATEWAYS).isArray()) {
            for (JsonNode node : object.get(PUBLIC_GATEWAYS)) {
                ObjectNode gateway = (ObjectNode) node;
                result = result && isIpAddress(gateway, GATEWAY_IP, MANDATORY);
                result = result && isMacAddress(gateway, GATEWAY_MAC, MANDATORY);
            }
        }

        if (object.get(CONTROLLERS) != null) {
            for (JsonNode jsonNode : object.get(CONTROLLERS)) {
                result = result && isController(jsonNode);
            }
        }
        return result;
    }

    private boolean isController(JsonNode jsonNode) {
        String[] ctrl = jsonNode.asText().split(":");
        final String error = "Malformed controller string " + jsonNode.asText() +
                ". Controller only takes a list of 'IP:port', 'IP', " +
                "or just one ':port'.";
        try {
            if (ctrl.length == 1) {
                IpAddress.valueOf(ctrl[INDEX_IP]);
                return true;
            }
            if (ctrl.length == 2 && ctrl[INDEX_IP].isEmpty() &&
                    object.get(CONTROLLERS).size() == 1) {
                TpPort.tpPort(Integer.valueOf(ctrl[INDEX_PORT]));
                return true;
            }
            if (ctrl.length == 2 && !ctrl[INDEX_IP].isEmpty()) {
                IpAddress.valueOf(ctrl[INDEX_IP]);
                TpPort.tpPort(Integer.valueOf(ctrl[INDEX_PORT]));
                return true;
            }
            throw new InvalidFieldException(CONTROLLERS, error);
        } catch (IllegalArgumentException e) {
            throw new InvalidFieldException(CONTROLLERS, error);
        }
    }

    /**
     * Returns the set of nodes read from network config.
     *
     * @return set of CordVtnNodeConfig or empty set
     */
    public Set<CordVtnNode> cordVtnNodes() {
        Set<CordVtnNode> nodes = Sets.newHashSet();
        JsonNode sshNode = object.get(SSH);
        String ovsdbPort = getConfig(object, OVSDB_PORT);

        object.get(CORDVTN_NODES).forEach(vtnNode -> {
            CidrAddr localMgmtIp = CidrAddr.valueOf(get(LOCAL_MANAGEMENT_IP, ""));
            CidrAddr hostsMgmtIp = CidrAddr.valueOf(getConfig(vtnNode, HOST_MANAGEMENT_IP));

            SshAccessInfo sshInfo = new SshAccessInfo(
                    hostsMgmtIp.ip().getIp4Address(),
                    TpPort.tpPort(Integer.parseInt(getConfig(sshNode, SSH_PORT))),
                    getConfig(sshNode, SSH_USER),
                    getConfig(sshNode, SSH_KEY_FILE));

            CordVtnNode.Builder nodeBuilder = DefaultCordVtnNode.builder()
                    .hostname(getConfig(vtnNode, HOSTNAME))
                    .hostManagementIp(hostsMgmtIp)
                    .localManagementIp(localMgmtIp)
                    .dataIp(CidrAddr.valueOf(getConfig(vtnNode, DATA_IP)))
                    .sshInfo(sshInfo)
                    .integrationBridgeId(DeviceId.deviceId(getConfig(vtnNode, INTEGRATION_BRIDGE_ID)))
                    .dataInterface(getConfig(vtnNode, DATA_IFACE));

            if (!Strings.isNullOrEmpty(ovsdbPort)) {
                nodeBuilder.ovsdbPort(TpPort.tpPort(Integer.parseInt(ovsdbPort)));
            }

            String hostMgmtIface = getConfig(vtnNode, HOST_MANAGEMENT_IFACE);
            if (!Strings.isNullOrEmpty(hostMgmtIface)) {
                nodeBuilder.hostManagementInterface(hostMgmtIface);
            }

            nodes.add(nodeBuilder.build());
        });

        return nodes;
    }

    /**
     * Gets the specified property as a string.
     *
     * @param jsonNode node whose fields to get
     * @param path property to get
     * @return value as a string
     */
    private String getConfig(JsonNode jsonNode, String path) {
        jsonNode = jsonNode.path(path);
        return jsonNode.asText();
    }

    /**
     * Returns public network gateway IP and MAC address pairs.
     *
     * @return map of ip and mac address
     */
    public Map<IpAddress, MacAddress> publicGateways() {
        JsonNode jsonNodes = object.get(PUBLIC_GATEWAYS);
        Map<IpAddress, MacAddress> publicGateways = Maps.newHashMap();

        if (jsonNodes == null) {
            return publicGateways;
        }

        jsonNodes.forEach(jsonNode -> publicGateways.put(
                IpAddress.valueOf(jsonNode.path(GATEWAY_IP).asText()),
                MacAddress.valueOf(jsonNode.path(GATEWAY_MAC).asText())));
        return publicGateways;
    }

    /**
     * Returns controllers for the integration bridge.
     * It returns the information taken from cluster service with the default OF
     * port if no controller is specified in the network config.
     *
     * @return list of controller information
     */
    public List<ControllerInfo> controllers() {
        List<ControllerInfo> ctrls = Lists.newArrayList();
        JsonNode ctrlNodes = object.get(CONTROLLERS);

        if (ctrlNodes == null || isCtrlPortOnly()) {
            ctrls = clusterService.getNodes().stream()
                    .map(ctrl -> new ControllerInfo(
                            ctrl.ip(),
                            ctrlNodes == null ? DEFAULT_OF_PORT : getCtrlPort(),
                            DEFAULT_OF_PROTOCOL))
                    .collect(Collectors.toList());
        } else {
            for (JsonNode ctrlNode : ctrlNodes) {
                String[] ctrl = ctrlNode.asText().split(":");
                ctrls.add(new ControllerInfo(
                        IpAddress.valueOf(ctrl[INDEX_IP]),
                        ctrl.length == 1 ? DEFAULT_OF_PORT :
                                Integer.parseInt(ctrl[INDEX_PORT]),
                        DEFAULT_OF_PROTOCOL));
            }
        }
        return ImmutableList.copyOf(ctrls);
    }

    private boolean isCtrlPortOnly() {
        if (object.get(CONTROLLERS).size() != 1) {
            return false;
        }
        JsonNode jsonNode = object.get(CONTROLLERS).get(0);
        String[] ctrl = jsonNode.asText().split(":");
        return ctrl.length == 2 && ctrl[INDEX_IP].isEmpty();
    }

    private int getCtrlPort() {
        JsonNode jsonNode = object.get(CONTROLLERS).get(0);
        String[] ctrl = jsonNode.asText().split(":");
        return Integer.parseInt(ctrl[INDEX_PORT]);
    }
}
