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

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Maps;
import org.apache.commons.lang3.tuple.Pair;
import org.onosproject.codec.JsonCodec;
import org.onosproject.net.domain.DomainId;
import org.onosproject.net.domain.DomainService;
import org.onosproject.cluster.ClusterService;
import org.onosproject.cluster.LeadershipService;
import org.onosproject.rest.AbstractWebResource;
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.DomainEndPoint;
import org.opencord.ce.api.models.EvcConnId;
import org.opencord.ce.api.services.MetroNetworkVirtualNodeService;
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.Map;
import java.util.Set;
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.DST_NI_LIST;
import static org.opencord.ce.api.services.channel.Symbols.FC;
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;
import static org.opencord.ce.api.services.channel.Symbols.SRC_NI;
import static org.opencord.ce.api.services.channel.Symbols.UNI;
import static org.onosproject.net.domain.DomainId.LOCAL;

/**
 *  Implementation of the listener methods for requesting {@link
 *  org.opencord.ce.api.models.CarrierEthernetForwardingConstruct} configuration.
 */
public enum HttpClientInstance
        implements MetroNetworkVirtualNodeService {
    INSTANCE;
    private final Logger log =
            LoggerFactory.getLogger(HttpClientInstance.class);

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

    private final Map<DomainId, Pair<Client, DomainEndPoint>> domainsEndPointsMap = Maps.newConcurrentMap();

    private final AbstractWebResource codecContext = new AbstractWebResource();

    private boolean configured = false;

    private ClusterService clusterService;
    private LeadershipService leadershipService;
    private DomainService domainService;

    private final ExecutorService networkExecutor =
            newSingleThreadExecutor();
           // newFixedThreadPool(5, groupedThreads("opencord/ecord-http", "event-handler"));

    public void configure(ClusterService clusterService, LeadershipService leadershipService,
                          DomainService domainService, Set<DomainEndPoint> endPoints) {

        if (!configured) {
            this.clusterService = clusterService;
            this.leadershipService = leadershipService;
            this.domainService = domainService;
            configured = true;
        }
        endPoints.forEach(siteConfig -> {
            DomainId domainId = siteConfig.domainId();
            synchronized (this) {
                domainsEndPointsMap.putIfAbsent(domainId,
                        Pair.of(createClient(), siteConfig));
                notify();
            }
        });
    }

    public Client getConnectionInfo(DomainId domainId) {
        return domainsEndPointsMap.get(domainId).getLeft();
    }

    private boolean isLeader(String topic) {
        return topic != null && 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;
        }
    }

    private String getTopic(DomainId domainId) {
        if (domainsEndPointsMap.get(domainId) != null) {
            return domainsEndPointsMap.get(domainId).getRight().topic();
        }
        log.info("Domain ID {} not among configured domain IDs", domainId.id());
        return null;
    }

    private Client createClient() {
        return ClientBuilder.newClient();
    }

    @Override
    public void setNodeForwarding(CarrierEthernetForwardingConstruct fc, CarrierEthernetNetworkInterface srcNi,
                                  Set<CarrierEthernetNetworkInterface> dstNiSet) {

        DomainId domainId = domainService.getDomain(srcNi.cp().deviceId());
        if (domainId == LOCAL || !isLeader(getTopic(domainId))) {
            return;
        }
        JsonCodec<CarrierEthernetForwardingConstruct> fcCodec =
                codecContext.codec(CarrierEthernetForwardingConstruct.class);
        JsonCodec<CarrierEthernetNetworkInterface> niCodec =
                codecContext.codec(CarrierEthernetNetworkInterface.class);
        ObjectNode body = codecContext.mapper().createObjectNode();

        body.set(FC, fcCodec.encode(fc, codecContext));
        body.set(SRC_NI, niCodec.encode(srcNi, codecContext));
        ArrayNode dstNiJsonArrayNode = codecContext.mapper().createArrayNode();
        dstNiSet.forEach(dstNi ->
                dstNiJsonArrayNode.add(niCodec.encode(dstNi, codecContext)));
        body.set(DST_NI_LIST, dstNiJsonArrayNode);
        String resource = "/ForwardingConstruct";
        networkExecutor.execute(new NetworkTask(domainId, POST, resource, body.toString(),
                new RequestCallback() {
                    @Override
                    public void onSuccess(Response response) {
                        log.info("FC request submit to domain: {}", domainId);

                    }

                    @Override
                    public void onError(Response response) {
                        log.error("FC call failure reason: {}", response.getStatusInfo());

                    }
                }));
    }

    @Override
    public void createBandwidthProfileResources(CarrierEthernetForwardingConstruct fc, CarrierEthernetUni uni) {
        DomainId domainId = domainService.getDomain(uni.cp().deviceId());
        if (domainId == LOCAL || !isLeader(getTopic(domainId))) {
            return;
        }
        String resource = "/createBwp";
        networkExecutor.execute(new NetworkTask(domainId, POST, resource,
                fcUniToRestBody(fc, uni).toString(),
                new RequestCallback() {
                    @Override
                    public void onSuccess(Response response) {
                        log.info("BW profile creation request submitted to domain: {}", domainId);
                    }

                    @Override
                    public void onError(Response response) {
                        log.error("BwProfile creation call fail: {}", response.getStatusInfo());

                    }
                }));
    }

    @Override
    public void applyBandwidthProfileResources(CarrierEthernetForwardingConstruct fc, CarrierEthernetUni uni) {
        DomainId domainId = domainService.getDomain(uni.cp().deviceId());
        if (domainId == LOCAL || !isLeader(getTopic(domainId))) {
            return;
        }
        String resource = "/applyBwp";
        networkExecutor.execute(new NetworkTask(domainId, POST, resource, fcUniToRestBody(fc, uni).toString(),
                new RequestCallback() {
                    @Override
                    public void onSuccess(Response response) {
                        log.info("BW profile activation request submitted to domain: {}", domainId);
                    }

                    @Override
                    public void onError(Response response) {
                        log.error("FAIL BW profile activation: {}", response.getStatusInfo());

                    }
                }));
    }

    @Override
    public void removeBandwidthProfileResources(CarrierEthernetForwardingConstruct fc, CarrierEthernetUni uni) {
        DomainId domainId = domainService.getDomain(uni.cp().deviceId());
        if (domainId == LOCAL || !isLeader(getTopic(domainId))) {
            return;
        }
        String resource = "/deleteBwp";
        networkExecutor.execute(new NetworkTask(domainId, POST, resource,
                fcUniToRestBody(fc, uni).toString(),
                new RequestCallback() {
                    @Override
                    public void onSuccess(Response response) {
                        log.info("BW profile creation request submitted to domain: {}", domainId);
                    }

                    @Override
                    public void onError(Response response) {
                        log.info("FAIL BW profile creation: {}", response.getStatusInfo());

                    }
                }));
    }

    @Override
    public void removeAllForwardingResources(EvcConnId fcId) {
        Set<DomainId> domainIds = domainService.getDomainIds();
        String resource = "/deleteFcResources/" + fcId.id();
        domainIds.forEach(domainId -> {
            if (isLeader(getTopic(domainId))) {
                networkExecutor.execute(new NetworkTask(domainId, DELETE, resource,
                        null, new RequestCallback() {
                            @Override
                            public void onSuccess(Response response) {

                            }

                            @Override
                            public void onError(Response response) {

                            }
                }));
            }
        });

    }

    private ObjectNode fcUniToRestBody(CarrierEthernetForwardingConstruct fc, CarrierEthernetUni uni) {
        JsonCodec<CarrierEthernetForwardingConstruct> fcCodec =
                codecContext.codec(CarrierEthernetForwardingConstruct.class);
        JsonCodec<CarrierEthernetNetworkInterface> niCodec =
                codecContext.codec(CarrierEthernetNetworkInterface.class);
        ObjectNode body = codecContext.mapper().createObjectNode();
        body.set(FC, fcCodec.encode(fc, codecContext));
        body.set(UNI, niCodec.encode(uni, codecContext));
        return body;
    }

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

        NetworkTask(DomainId domainId, String method, String resource, String body,
                    RequestCallback callback) {
            this.domainId = domainId;
            this.method = method;
            this.resource = resource;
            this.body = body;
            this.callback = callback;
        }
        @Override
        public void run() {

            synchronized (this) {
                while (domainsEndPointsMap.get(domainId) == null) {
                    log.info("End point object missing for domain {}." +
                            "\nPossibly the Bundle configuration is missing ", domainId);
                    try {
                        wait();
                    } catch (InterruptedException ie) {
                        log.info("Interrupted exception: " + ie.getMessage());
                    }

                }
            }
            DomainEndPoint endPoint = domainsEndPointsMap.get(domainId).getRight();

            Client client = domainsEndPointsMap.get(domainId).getLeft();

            String url = HTTP + COLON + DOUBLESLASH + endPoint.publicIp().toString() + COLON +
                    endPoint.port() + BASE_URL + "/carrierethernet" + resource;
            log.info("DEBUG {}: Sending data via http: {}", 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);
            }
        }
    }
}