/*
 * Copyright Avaya Inc., All Rights Reserved.
 *
 * THIS IS UNPUBLISHED PROPRIETARY SOURCE CODE OF Avaya Inc.
 *
 * The copyright notice above does not evidence any actual or intended
 * publication of such source code.
 *
 * Some third-party source code components may have been modified from their
 * original versions by Avaya Inc.
 *
 * The modifications are Copyright Avaya Inc., All Rights Reserved.
 */
package com.avaya.oceanalytics.openinterface.websocketclient;

import com.avaya.oceanalytics.openinterface.websocketclient.auth.AuthenticationTokenCache;
import com.avaya.oceanalytics.openinterface.websocketclient.auth.UserCredentials;
import com.avaya.oceanalytics.openinterface.websocketclient.subscription.StreamType;
import com.avaya.oceanalytics.openinterface.websocketclient.subscription.*;
import com.avaya.oceanalytics.openinterface.websocketclient.websocket.RealtimeDataProvider;
import com.avaya.oceanalytics.openinterface.websocketclient.websocket.RealtimeDataProviderException;
import com.avaya.oceanalytics.openinterface.websocketclient.subscription.SubscriptionProducer;
import com.avaya.oceanalytics.openinterface.websocketclient.websocket.SuppressibleRealtimeDataHandler;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;

import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Scanner;
import java.util.Set;

/**
 * Handler for internal shell commands inputted by user.
 *
 * @author seprokof
 * @author Ivan Kovalev
 *
 */
@ShellComponent
public class ShellCommandHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(ShellCommandHandler.class);

    private static final String ANNOUNCEMENT = "You are about to get realtime data from Avaya Analytics."
            + " Type something and press ENTER to return to prompt.";

    private final RestClient restClient;
    private final ObjectFactory<UserCredentials> credentialsFactory;
    private final AuthenticationTokenCache authTokenCache;
    private final SubscriptionManager manager;
    private final SubscriptionProducer producer;
    private final SuppressibleRealtimeDataHandler rtdHandler;
    private final RealtimeDataProvider rtdProvider;

    private final OutputStream outputStream;
    private final OutputStream errStream;
    private final Scanner inputScanner;

    @Autowired
    public ShellCommandHandler(RestClient restClient, ObjectFactory<UserCredentials> credentialsFactory,
                               AuthenticationTokenCache authTokenCache, SubscriptionManager manager,
                               SuppressibleRealtimeDataHandler rtdHandler, SubscriptionProducer producer, RealtimeDataProvider rtdProvider) {
        this.restClient = restClient;
        this.credentialsFactory = credentialsFactory;
        this.authTokenCache = authTokenCache;
        this.manager = manager;
        this.outputStream = System.out; //NOSONAR
        this.errStream = System.err; //NOSONAR
        this.inputScanner = new Scanner(System.in);
        this.rtdHandler = rtdHandler;
        this.producer = producer;
        this.rtdProvider = rtdProvider;
    }

    /**
     * 'list-subscriptions' command handler
     *
     * @param sourceId
     *            the unique identifier of data source
     * @param username
     *            of request initiator
     * @param password
     *            of request initiator
     * @param detailed
     *             flag of detailed information
     */

    @ShellMethod(value = "Shows list of active subscriptions.", key = "list-subscriptions")
    public void listSubscriptions(@ShellOption(value = {"-src","--source-id"}) String sourceId,
                                  @ShellOption(value = { "-u", "--username" }) String username,
                                  @ShellOption(value = { "-p", "--password" }) String password,
                                  @ShellOption(value = { "-d", "--detailed" }) Boolean detailed) {


        printMessage("Following subscriptions are active for specified credentials");
        try{
            String token = obtainAuthToken(username, password);
            List<SubscriptionResponse> subscriptionsFromServer = restClient.getListOfSubscriptions(token, sourceId);
            printMessage(subscriptionsFromServer.size() + " subscriptions" + (subscriptionsFromServer.size() == 1 ? "" : "s") + " found.");
            if (detailed) {
                subscriptionsFromServer.forEach(s -> printMessage("\t" + s.toString() + System.lineSeparator()));
            } else {
                subscriptionsFromServer.stream().map(s -> "\t" + s.getProducerId()).forEach(this::printMessage);
            }
        } catch (RestClientException e) {
            LOGGER.error("Exception while handling 'list' command: {}", e);
            printError("Unable to retrieve a list of subscriptions from server");
        }

        Set<String> activeSubscriptions = manager.getAllSubscriptions();
        printMessage("Following subscriptions are active within the current client session");
        printMessage(activeSubscriptions.size() + " subscriptions" + (activeSubscriptions.size() == 1 ? "" : "s") + " found.");
        for (String producerId : activeSubscriptions){
            Subscription subscription = manager.getSubscription(producerId);
            printMessage(subscription.toString());
        }
    }

    /**
     * 'list-sources' command handler
     *
     * @param username
     *            of request initiator
     * @param password
     *            of request initiator
     * @param detailed
     *             flag of detailed information
     */

    @ShellMethod(value = "Shows list of sources.", key = "list-sources")
    public void listSources(@ShellOption(value = { "-u", "--username" }) String username,
                                  @ShellOption(value = { "-p", "--password" }) String password,
                                  @ShellOption(value = { "-d", "--detailed" }) Boolean detailed) {


        printMessage("Sources configured in the system.");
        try{
            String token = obtainAuthToken(username, password);
            List<Source> sources = restClient.getSources(token);
            printMessage(sources.size() + " sources" + (sources.size() == 1 ? "" : "s") + " found.");
            if (detailed) {
                sources.forEach(s -> printMessage("\t" + s.toString() + System.lineSeparator()));
            } else {
                sources.stream().map(s -> "\t" + s.getSourceId()).forEach(this::printMessage);
            }
        } catch (RestClientException e) {
            LOGGER.error("Exception while handling 'list-sources' command: {}", e);
            printError("Unable to retrieve a list of sources from server");
        }
    }


    /**
     * 'get-source' command handler
     *
     * @param sourceId
     *            the unique identifier of data source
     * @param username
     *            of request initiator
     * @param password
     *            of request initiator
     * @param detailed
     *             flag of detailed information
     */

    @ShellMethod(value = "Shows a source details.", key = "get-source")
    public void getSource(@ShellOption({ "-src", "--source-id" }) String sourceId,
                          @ShellOption(value = { "-u", "--username" }) String username,
                            @ShellOption(value = { "-p", "--password" }) String password,
                            @ShellOption(value = { "-d", "--detailed" }) Boolean detailed) {


        printMessage("Source configured in the system.");
        try{
            String token = obtainAuthToken(username, password);
            Source source = restClient.getSource(token, sourceId);
            if (detailed) {
                printMessage("\t" + source.toString() + System.lineSeparator());
            } else {
                printMessage("\t" + source.getSourceId() + System.lineSeparator());
            }
        } catch (RestClientException e) {
            LOGGER.error("Exception while handling 'get-source' command: {}", e);
            printError("Unable to retrieve the source from server");
        }
    }

    /**
     * 'list-producers' command handler
     *
     * @param sourceId
     *            the unique identifier of data source
     * @param username
     *            of request initiator
     * @param password
     *            of request initiator
     * @param detailed
     *             flag of detailed information
     */

    @ShellMethod(value = "Shows list of producers.", key = "list-producers")
    public void listProducers(@ShellOption({ "-src", "--source-id" }) String sourceId,
                            @ShellOption(value = { "-u", "--username" }) String username,
                            @ShellOption(value = { "-p", "--password" }) String password,
                            @ShellOption(value = { "-d", "--detailed" }) Boolean detailed) {


        printMessage("Producers configured in the system.");
        try{
            String token = obtainAuthToken(username, password);
            List<Producer> producers = restClient.getProducers(token, sourceId);
            printMessage(producers.size() + " producers" + (producers.size() == 1 ? "" : "s") + " found.");
            if (detailed) {
                producers.forEach(s -> printMessage("\t" + s.toString() + System.lineSeparator()));
            } else {
                producers.stream().map(s -> "\t" + s.getProducerId()).forEach(this::printMessage);
            }
        } catch (RestClientException e) {
            LOGGER.error("Exception while handling 'list-producers' command: {}", e);
            printError("Unable to retrieve a list of producers from server");
        }
    }

    /**
     * 'get-producer' command handler
     *
     * @param sourceId
     *            the unique identifier of data source
     * @param producerId
     *            the unique identifier of data producer
     * @param username
     *            of request initiator
     * @param password
     *            of request initiator
     * @param detailed
     *             flag of detailed information
     */

    @ShellMethod(value = "Shows a producer details.", key = "get-producer")
    public void getProducer(@ShellOption({ "-src", "--source-id" }) String sourceId,
                          @ShellOption(value = { "-pr", "--producer-id" }) String producerId,
                          @ShellOption(value = { "-u", "--username" }) String username,
                          @ShellOption(value = { "-p", "--password" }) String password,
                          @ShellOption(value = { "-d", "--detailed" }) Boolean detailed) {


        printMessage("Producer configured in the system.");
        try{
            String token = obtainAuthToken(username, password);
            Producer producer = restClient.getProducer(token, sourceId, producerId);
            if (detailed) {
                printMessage("\t" + producer.toString() + System.lineSeparator());
            } else {
                printMessage("\t" + producer.getSourceId() + System.lineSeparator());
            }
        } catch (RestClientException e) {
            LOGGER.error("Exception while handling 'get-producer' command: {}", e);
            printError("Unable to retrieve the producer from server");
        }
    }

    /**
     * 'get-dictionary' command handler
     *
     * @param sourceId
     *            the unique identifier of data source
     * @param producerId
     *            the unique identifier of data producer
     * @param locale
     *            the locale. For example {en-us, de, fr, it, es, ko, ja, ru, pt_BR, zh-cn, zh-tw}
     * @param username
     *            of request initiator
     * @param password
     *            of request initiator
     * @param detailed
     *             flag of detailed information
     */

    @ShellMethod(value = "Shows the dictionaries for a producer.", key = "get-dictionary")
    public void getDictionary(@ShellOption({ "-src", "--source-id" }) String sourceId,
                            @ShellOption(value = { "-pr", "--producer-id" }) String producerId,
                            @ShellOption(value = { "-l", "--locale" }) String locale,
                            @ShellOption(value = { "-u", "--username" }) String username,
                            @ShellOption(value = { "-p", "--password" }) String password,
                            @ShellOption(value = { "-d", "--detailed" }) Boolean detailed) {


        printMessage("Dictionary associated with Producer: " + producerId + " in locale: " + locale);
        try{
            String token = obtainAuthToken(username, password);
            Dictionary dictionary = restClient.getDictionary(token, sourceId, producerId, locale);
            if (detailed) {
                printMessage("\t" + dictionary.toString() + System.lineSeparator());
            } else {
                printMessage("\t" + dictionary.getDescription() + System.lineSeparator());
            }
        } catch (RestClientException e) {
            LOGGER.error("Exception while handling 'get-producer' command: {}", e);
            printError("Unable to retrieve the producer from server");
        }
    }

    /**
     * 'get-dimension-data' command handler
     *
     * @param sourceId
     *            the unique identifier of data source
     * @param dimensionDataName
     *            the name of the dimension data
     * @param username
     *            of request initiator
     * @param password
     *            of request initiator
     * @param detailed
     *             flag of detailed information
     */

    @ShellMethod(value = "Shows the dimension data for a source.", key = "get-dimension-data")
    public void getDimensionData(@ShellOption({ "-src", "--source-id" }) String sourceId,
                              @ShellOption({ "-dim", "--dimension-data-name" }) String dimensionDataName,
                              @ShellOption(value = { "-u", "--username" }) String username,
                              @ShellOption(value = { "-p", "--password" }) String password,
                              @ShellOption(value = { "-d", "--detailed" }) Boolean detailed) {


        printMessage("Dimension data associated with Source: " + sourceId);
        try{
            String token = obtainAuthToken(username, password);
            DimensionData dimensionData = restClient.getDimensionData(token, sourceId, dimensionDataName);
            if (detailed) {
                printMessage("\t" + dimensionData.toString() + System.lineSeparator());
            } else {
                printMessage("\t" + dimensionData.getDimensions().toString() + System.lineSeparator());
            }
        } catch (RestClientException e) {
            LOGGER.error("Exception while handling 'get-dimension-data' command: {}", e);
            printError("Unable to retrieve the dimension data from server");
        }
    }

    /**
     * 'subscribe' command handler.
     *
     * @param sourceId
     *            the unique identifier of data source
     * @param producerId
     *            the unique identifier of producer
     * @param streamType
     *            the type of RTD stream. 'SoD' and 'MW' are only supported
     * @param tenantId
     *            the unique identifier of tenant. Will be '0' if not explicitly specified
     * @param username
     *            of request initiator
     * @param password
     *            of request initiator
     */
    @ShellMethod(value = "Subscribes for realtime data.", key = "subscribe")
    public void subscribeForRealtimeData(@ShellOption({ "-src", "--source-id" }) String sourceId,
                                         @ShellOption(value = { "-pr", "--producer-id" }) String producerId,
                                         @ShellOption(value = { "-st", "--stream-type" }) StreamType streamType,
                                         @ShellOption(value = { "-t", "--tenant-id" }, defaultValue = "0") String tenantId,
                                         @ShellOption(value = { "-u", "--username" }) String username,
                                         @ShellOption(value = { "-p", "--password" }) String password) {

        try {
            String token = obtainAuthToken(username, password);

            Filter dimensionFilter = new Filter();
            dimensionFilter.setName("routingServiceName");
            dimensionFilter.getFilters().add("");

            Filter measureFilter = new Filter();
            measureFilter.setName("measure");
            measureFilter.getFilters().add("");

            SubscriptionRequest request = new SubscriptionRequest();
            request.setProducerId(producerId);
            request.setSourceId(sourceId);
            request.setStreamType(streamType.toString());
            request.setTenantId(tenantId);
            request.setTransport("websocket");
            request.setUserName(username);
            request.getDimensionFilters().add(dimensionFilter);
            request.getMeasureFilters().add(measureFilter);

            SubscriptionResponse response = restClient.subscribeForRealtimeData(token, request);
            LOGGER.debug("Successfully subscribed for '{}' stream provided by '{}' from the '{}'", streamType,
                    producerId, sourceId);

            if(!rtdProvider.isAlive()) {
                rtdProvider.connect(response.getEndpoint(), rtdHandler);
                LOGGER.debug("Successfully connected to '{}'", response.getEndpoint());
            }

            Subscription subscription = new Subscription();
            subscription.setProducer(response.getProducer());
            subscription.setProducerId(response.getProducerId());
            subscription.setSource(response.getSource());
            subscription.setSourceId(response.getSourceId());
            subscription.setStreamType(response.getStreamType());
            subscription.setTransport(response.getTransport());
            subscription.setEndpoint(response.getEndpoint());
            subscription.setGuid(response.getGuid());
            subscription.setRtdProvider(rtdProvider);

            printMessage("Subscription:" + subscription);

            manager.associate(response.getProducerId(), subscription);

            printMessage(ANNOUNCEMENT);
            rtdHandler.suppress(false);
            producer.sendSubscribeRequest(obtainAuthToken(username, password),subscription);

            inputScanner.next();
            rtdHandler.suppress(true);
        } catch (Exception e) {
            LOGGER.error("Unexpected exception while handling 'subscribe' command: {}", e);
            printError(e.getMessage());
        }
    }

    /**
     * 'unsubscribe' command handler.
     *
     * @param producerId
     *            the unique identifier of producer
     * @param username
     *            of request initiator
     * @param password
     *            of request initiator
     */

    @ShellMethod(value = "Unsubscribe from realtime data.", key = "unsubscribe")
    public void unsubscribeForRealtimeData(@ShellOption(value = { "-pr", "--producer-id" }) String producerId,
                                           @ShellOption(value = { "-u", "--username" }) String username,
                                           @ShellOption(value = { "-p", "--password" }) String password) {

        try {
            LOGGER.trace("try sendUnsubscribeRequest from " + producerId + " ...");
            Subscription subscription = manager.getSubscription(producerId);
            if (subscription == null) {
                printError("Subscription matching the specified criteria was not found");
                return;
            }

            printMessage("\t Sending unsubscribe message to Producer over WebSocket");
            String token = obtainAuthToken(username, password);
            producer.sendUnsubscribeRequest(token,subscription);

            printMessage("\t Sending unsubscribe message to REST interface");
            restClient.unsubscribeFromRealtimeData(token,subscription.getSourceId(),subscription.getProducerId());

            printMessage("Successfully unsubscribed from producer " + producerId);

            manager.dissociate(producerId);

            // Disconnect if there is no more subscribers
            Set<String> allSubscriptions = manager.getAllSubscriptions();
            if (allSubscriptions.size()==0){
                subscription.getRtdDataProvider().disconnect();
            }
        }
        catch (RealtimeDataProviderException e) {
            LOGGER.error("Exception while sending WebSocket unsubscription request",e);
        }
        catch (Exception e) {
            LOGGER.error("Unexpected exception while handling 'unsubscribe' command:",e);
        }
    }

    /**
     * 'pumpup' command handler.
     *
     * @param producerId
     *            the unique identifier of producer
     * @param username
     *            of request initiator
     * @param password
     *            of request initiator
     */

    @ShellMethod(value = "Sends pumpup request for specified measure.", key = "pumpup")
    public void pumpup(@ShellOption(value = { "-pr", "--producer-id" }) String producerId,
                       @ShellOption(value = {"-u","--username"}) String username,
                       @ShellOption(value = {"-p","--password"}) String password){

        try {
            Subscription subscription =manager.getSubscription(producerId);
            if(subscription != null) {

                producer.sendPumpupRequest(obtainAuthToken(username, password),subscription);

                printMessage("Successfully sent pumpup request for producerId: " + producerId);
                LOGGER.info("Successfully sent pumpup request for producerId: {} ", producerId);

            } else {
                printError("Subscription not found");
            }

        } catch (Exception e) {
            LOGGER.error("Unexpected exception while handling 'pumpup' command: {}", e);
            printError(e.getMessage());
        }
    }

    /**
     * 'resume' command handler
     */

    @ShellMethod(value = "Resumes a realtime data streaming for all active subscriptions", key = "resume")
    public void resume(){

        if(!manager.getAllSubscriptions().isEmpty()) {

            rtdHandler.suppress(false);
            printMessage(ANNOUNCEMENT);
            inputScanner.next();
            rtdHandler.suppress(true);
        } else {
            printError("There are no active subscriptions");
        }
    }

    private String obtainAuthToken(String username, String password) {
        UserCredentials credentials = credentialsFactory.getObject();
        credentials.setUsername(username);
        credentials.setPassword(password);
        return authTokenCache.getToken(credentials);
    }

    private void printMessage(String text) {
        printMessageToStream(text, outputStream);
    }

    private void printError(String text) {
        printMessageToStream(text, errStream);
    }

    private void printMessageToStream(String text, OutputStream stream) {
        if (StringUtils.isNotEmpty(text)) {
            try {
                if (!StringUtils.endsWith(text, System.lineSeparator())) {
                    text += System.lineSeparator();
                }
                stream.write(text.getBytes());
            } catch (IOException e) {
                LOGGER.error("Unable to output message", e);
            }
        }
    }

}
