/*
 * 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.channel.client;

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.onlab.packet.ChassisId;
import org.onosproject.cluster.ClusterService;
import org.onosproject.cluster.LeadershipService;
import org.onosproject.codec.JsonCodec;
import org.onosproject.net.DefaultAnnotations;
import org.onosproject.net.DefaultDevice;
import org.onosproject.net.DefaultPort;
import org.onosproject.net.DeviceId;
import org.onosproject.net.Port;
import org.onosproject.net.device.PortDescription;
import org.onosproject.net.domain.DomainId;
import org.onosproject.net.provider.ProviderId;
import org.onosproject.rest.AbstractWebResource;
import org.opencord.ce.api.models.DomainEndPoint;
import org.opencord.ce.api.services.channel.RequestCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.List;
import java.util.concurrent.ExecutorService;

import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static org.opencord.ce.api.services.channel.Symbols.BASE_URL;
import static org.opencord.ce.api.services.channel.Symbols.COLON;
import static org.opencord.ce.api.services.channel.Symbols.DELETE;
import static org.opencord.ce.api.services.channel.Symbols.DOUBLESLASH;
import static org.opencord.ce.api.services.channel.Symbols.GET;
import static org.opencord.ce.api.services.channel.Symbols.HTTP;
import static org.opencord.ce.api.services.channel.Symbols.POST;

/**
 * HTTP client instance used to communicate
 * to the global ONOS events related to the big switch topology abstraction.
 */
public enum HttpClientInstance {
    INSTANCE;
    private final Logger log =
            LoggerFactory.getLogger(HttpClientInstance.class);
    private static final Object MONITOR = new Object();

    private static final int STATUS_OK = Response.Status.OK.getStatusCode();
    private static final int STATUS_REQ_UNPROCESSABLE = Response.Status.NOT_ACCEPTABLE.getStatusCode();

    private final AbstractWebResource codecContext = new AbstractWebResource();
    private final Client client = ClientBuilder.newClient();

    private ClusterService clusterService;
    private LeadershipService leadershipService;
    private DomainEndPoint globalOnos;

    private final ExecutorService networkExecutor =
            newSingleThreadExecutor();

    public void configure(ClusterService clusterService, LeadershipService leadershipService) {
            this.clusterService = clusterService;
            this.leadershipService = leadershipService;
    }

    protected void setGlobalOnos(DomainEndPoint globalOnos) {
        synchronized (MONITOR) {
            this.globalOnos = globalOnos;
        }
    }

    private boolean isLeader(String topic) {
        return leadershipService.getLeader(topic).id()
                .equals(clusterService.getLocalNode().id().id());
    }

    private boolean checkReply(Response response) {
        if (response != null) {
            return checkStatusCode(response.getStatus());
        }
        log.error("Null reply from end point");
        return false;
    }

    private boolean checkStatusCode(int statusCode) {
        if (statusCode == STATUS_OK) {
            return true;
        } else {
            log.error("Failed request, HTTP error code : "
                    + statusCode);
            return false;
        }
    }

    public void notifyBigSwitch(DomainId siteId) {
        String deviceId = "domain:" + siteId.id();
        String resource = "/" + siteId + "/" + deviceId;
        ObjectNode body = codecContext.mapper().createObjectNode();
        // body is empty for now
        networkExecutor.execute(new NetworkTask(POST, resource, body.toString(), new RequestCallback() {
            @Override
            public void onSuccess(Response response) {

            }

            @Override
            public void onError(Response response) {

            }
        }));
    }

    public void notifyBigSwitchPorts(DomainId siteId, List<PortDescription> ports) {
        JsonCodec<Port> portCodec = codecContext.codec(Port.class);
        ArrayNode body = codecContext.mapper().createArrayNode();
        DeviceId deviceId = DeviceId.deviceId("domain:" + siteId.id());
        ports.forEach(portDescription ->
            body.add(portCodec.encode(new DefaultPort(new DummyDevice(deviceId), portDescription.portNumber(),
                    portDescription.isEnabled(), portDescription.type(),
                    portDescription.portSpeed(), portDescription.annotations()), codecContext)));
        String resource = "/" + siteId + "/" + deviceId + "/ports";
        networkExecutor.execute(new NetworkTask(POST, resource, body.toString(), new RequestCallback() {
            @Override
            public void onSuccess(Response response) {

            }

            @Override
            public void onError(Response response) {

            }
        }));

    }

    private class NetworkTask implements Runnable {
        private String method;
        private String resource;
        private String body;
        private RequestCallback callback;

        NetworkTask(String method, String resource, String body,
                    RequestCallback callback) {
            this.method = method;
            this.resource = resource;
            this.body = body;
            this.callback = callback;
        }
        @Override
        public void run() {
            synchronized (MONITOR) {
                while (globalOnos == null) {
                    try {
                        log.info("wait() global ONOS endpoint");
                        MONITOR.wait();
                    } catch (InterruptedException ie) {
                        log.info("Interrupted exception: " + ie.getMessage());
                    }
                }
            }
            if (!isLeader(globalOnos.topic())) {
                return;
            }
            String url = HTTP + COLON + DOUBLESLASH + globalOnos.publicIp() + COLON +
                    globalOnos.port() + BASE_URL + "/global/topology" + resource;

            log.info("Sending data via http: url: {}\n body: {}", url, body);

            WebTarget webTarget = client.target(url);
            Response response;
            switch (method) {
                case POST:
                    response = webTarget.request(MediaType.APPLICATION_JSON)
                            .post(Entity.entity(body, MediaType.APPLICATION_JSON));
                    break;
                case DELETE:
                    response = webTarget.request(MediaType.APPLICATION_JSON).delete();
                    break;
                case GET:
                default:
                    response = webTarget.request(MediaType.APPLICATION_JSON).get();
                    break;
            }

            if (!checkReply(response)) {
                callback.onError(response);
            } else {
                callback.onSuccess(response);
            }
        }
    }

    /**
     * Dummy Device which only holds DeviceId.
     */
    private static final class DummyDevice extends DefaultDevice {
        /**
         * Constructs Dummy Device which only holds DeviceId.
         *
         * @param did device Id
         */
        public DummyDevice(DeviceId did) {
            super(new ProviderId(did.uri().getScheme(), "PortCodec"), did,
                    Type.SWITCH, "dummy", "0", "0", "0", new ChassisId(),
                    DefaultAnnotations.EMPTY);
        }
    }
}
