/*
 * Copyright 2016-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.rest;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import org.glassfish.jersey.client.ClientProperties;
import org.onlab.util.Tools;
import org.onosproject.rest.AbstractWebResource;
import org.opencord.cordvtn.api.net.NetworkId;
import org.opencord.cordvtn.api.net.PortId;
import org.opencord.cordvtn.api.net.ServiceNetwork;
import org.opencord.cordvtn.api.net.ServicePort;
import org.slf4j.Logger;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.Set;
import java.util.stream.Collectors;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.net.MediaType.JSON_UTF_8;
import static java.net.HttpURLConnection.HTTP_OK;
import static org.slf4j.LoggerFactory.getLogger;

/**
 * Implementation of REST client for XOS VTN networking service.
 */
public final class XosVtnNetworkingClient extends AbstractWebResource {

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

    private static final String URL_BASE = "/api/service/vtn/";
    private static final String URL_SERVICE_NETWORKS = "serviceNetworks/";
    private static final String URL_SERVICE_PORTS = "servicePorts/";

    private static final String SERVICE_PORTS = "servicePorts";
    private static final String SERVICE_PORT  = "servicePort";
    private static final String SERVICE_NETWORKS = "serviceNetworks";
    private static final String SERVICE_NETWORK  = "serviceNetwork";
    private static final String EMPTY_JSON_STRING = "{}";

    private static final String MSG_RECEIVED = "Received ";
    private static final String ERR_LOG = "Received %s result with wrong format: %s";

    private static final int DEFAULT_TIMEOUT_MS = 2000;

    private final String endpoint;
    private final String user;
    private final String password;
    private final Client client = ClientBuilder.newClient();

    private XosVtnNetworkingClient(String endpoint, String user, String password) {
        this.endpoint = endpoint;
        this.user = user;
        this.password = password;

        client.property(ClientProperties.CONNECT_TIMEOUT, DEFAULT_TIMEOUT_MS);
        client.property(ClientProperties.READ_TIMEOUT, DEFAULT_TIMEOUT_MS);
        mapper().enable(SerializationFeature.INDENT_OUTPUT);
    }

    public Set<ServiceNetwork> serviceNetworks() {
        String response = restGet(URL_SERVICE_NETWORKS);
        final String error = String.format(ERR_LOG, SERVICE_NETWORKS, response);
        try {
            JsonNode jsonTree = mapper().readTree(response).get(SERVICE_NETWORKS);
            if (jsonTree == null) {
                return ImmutableSet.of();
            }
            log.trace(MSG_RECEIVED + SERVICE_NETWORKS);
            log.trace(mapper().writeValueAsString(jsonTree));
            return Tools.stream(jsonTree).map(snet -> codec(ServiceNetwork.class)
                    .decode((ObjectNode) snet, this))
                    .collect(Collectors.toSet());
        } catch (IOException e) {
            throw new IllegalArgumentException(error);
        }
    }

    public ServiceNetwork serviceNetwork(NetworkId netId) {
        String response = restGet(URL_SERVICE_NETWORKS + netId.id());
        final String error = String.format(ERR_LOG, SERVICE_NETWORK, response);
        try {
            JsonNode jsonTree = mapper().readTree(response).get(SERVICE_NETWORK);
            if (jsonTree == null) {
                throw new IllegalArgumentException(error);
            }
            log.trace(MSG_RECEIVED + SERVICE_NETWORK);
            log.trace(mapper().writeValueAsString(jsonTree));
            return codec(ServiceNetwork.class).decode((ObjectNode) jsonTree, this);
        } catch (IOException e) {
            throw new IllegalArgumentException(error);
        }
    }

    public Set<ServicePort> servicePorts() {
        String response = restGet(URL_SERVICE_PORTS);
        final String error = String.format(ERR_LOG, SERVICE_PORTS, response);
        try {
            JsonNode jsonTree = mapper().readTree(response).get(SERVICE_PORTS);
            if (jsonTree == null) {
                ImmutableSet.of();
            }
            log.trace(MSG_RECEIVED + SERVICE_PORTS);
            log.trace(mapper().writeValueAsString(jsonTree));
            return Tools.stream(jsonTree).map(sport -> codec(ServicePort.class)
                    .decode((ObjectNode) sport, this))
                    .collect(Collectors.toSet());
        } catch (IOException e) {
            throw new IllegalArgumentException(error);
        }
    }

    public ServicePort servicePort(PortId portId) {
        String response = restGet(URL_SERVICE_PORTS + portId.id());
        final String error = String.format(ERR_LOG, SERVICE_PORT, response);
        try {
            JsonNode jsonTree = mapper().readTree(response).get(SERVICE_PORT);
            if (jsonTree == null) {
                throw new IllegalArgumentException(error);
            }
            log.trace(MSG_RECEIVED + SERVICE_PORT);
            log.trace(mapper().writeValueAsString(jsonTree));
            return codec(ServicePort.class).decode((ObjectNode) jsonTree, this);
        } catch (IOException e) {
            throw new IllegalArgumentException(error);
        }
    }

    private String restGet(String path) {
        WebTarget wt = client.target(endpoint + URL_BASE).path(path);
        Invocation.Builder builder = wt.request(JSON_UTF_8.toString());
        try {
            Response response = builder.get();
            if (response.getStatus() != HTTP_OK) {
                log.warn("Failed to get resource {}", endpoint + URL_BASE + path);
                return EMPTY_JSON_STRING;
            }
        } catch (javax.ws.rs.ProcessingException e) {
            return EMPTY_JSON_STRING;
        }
        return builder.get(String.class);
    }

    /**
     * Returns endpoint url for the XOS service API access.
     *
     * @return endpoint url as a string
     */
    public String endpoint() {
        return endpoint;
    }

    /**
     * Returns user name for the XOS service API access.
     *
     * @return user name as a string
     */
    public String user() {
        return user;
    }

    /**
     * Returns password for the XOS service API access.
     *
     * @return password as a string
     */
    public String password() {
        return password;
    }

    /**
     * Returns new XOS service networking builder instance.
     *
     * @return xos service networking builder
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Builder of the XOS service networking entities.
     */
    public static final class Builder {

        private String endpoint;
        private String user;
        private String password;

        private Builder() {
        }

        /**
         * Builds immutable XOS service networking instance.
         *
         * @return xos service networking instance
         */
        public XosVtnNetworkingClient build() {
            checkArgument(!Strings.isNullOrEmpty(endpoint));
            checkArgument(!Strings.isNullOrEmpty(user));
            checkArgument(!Strings.isNullOrEmpty(password));

            // TODO perform authentication when XOS provides it
            return new XosVtnNetworkingClient(endpoint, user, password);
        }

        /**
         * Returns XOS service networking builder with the supplied endpoint.
         *
         * @param endpoint endpoint url
         * @return xos service networking builder
         */
        public Builder endpoint(String endpoint) {
            this.endpoint = endpoint;
            return this;
        }

        /**
         * Returns XOS service networking builder with the supplied user
         * credential.
         *
         * @param user user
         * @return xos service networking builder
         */
        public Builder user(String user) {
            this.user = user;
            return this;
        }

        /**
         * Returns XOS service networking builder with the supplied password
         * credential.
         *
         * @param password password
         * @return xos service networking builder
         */
        public Builder password(String password) {
            this.password = password;
            return this;
        }
    }
}
