/*
 * 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.AuthenticationResponse;
import com.avaya.oceanalytics.openinterface.websocketclient.auth.AuthenticationToken;
import com.avaya.oceanalytics.openinterface.websocketclient.subscription.*;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.*;
import org.springframework.web.util.UriComponentsBuilder;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

/**
 * Rest client to interact with Realtime Open Interface exposed by Avaya Analytics.
 *
 * @author seprokof
 *
 */
@Component
public class RestClient {
    private static final Logger LOGGER = LoggerFactory.getLogger(RestClient.class);
    private static final String LOG_RECEIVE_RESPONSE = "Received response <- {}";

    private static final String DEFAULT_HOST = "localhost";
    private static final int DEFAULT_PORT = 8443;
    private static final int MAX_PORT_VALUE = 65535;

    private static final String SCHEME_HTTPS = "https";
    private static final String PS_DOMAIN = "orca-streams-rest";
    private static final String P_LOGIN = "/users/login";
    private static final String QP_USERNAME = "username";
    private static final String QP_PASSWORD = "password"; //NOSONAR
    private static final String PS_SOURCES = "sources";
    private static final String PS_PRODUCERS = "producers";
    private static final String PS_SUBSCRIPTIONS = "subscriptions";
    private static final String PS_DICTIONARIES = "dictionaries";
    private static final String PS_ADMIN_DATA = "Admin_Admin_3.5";
    private static final String PS_DIM_DATA = "dimdata";
    private static final String PS_DATA_TYPE = "dataType";


    private final String hostname;
    private final Integer port;
    private final RestTemplate restTemplate;


    @Autowired
    public RestClient(RestTemplate restTemplate,
                      @Value("${avaya.oceanalytics.stream-server.hostname:" + DEFAULT_HOST + "}") String hostname,
                      @Value("${avaya.oceanalytics.stream-server.port:" + DEFAULT_PORT + "}") Integer port) {
        this.restTemplate = restTemplate;
        this.hostname = StringUtils.isBlank(hostname) ? DEFAULT_HOST : hostname;
        this.port = (port == null || port < 0 || port > MAX_PORT_VALUE) ? DEFAULT_PORT : port;
    }

    /**
     * Authenticates user with specified username and password.
     *
     * @param username
     *            of request initiator
     * @param password
     *            of request initiator
     * @return expiring authentication token
     * @throws RestClientException
     *             will be thrown in case of any failure during sending or if the server replied with an error
     */
    public AuthenticationToken authenticate(String username, String password) {
        String url = getHttpUrl().pathSegment(PS_DOMAIN).path(P_LOGIN).queryParam(QP_USERNAME, username).queryParam(QP_PASSWORD, password)
                .toUriString();
        try {
            LOGGER.trace("Executing GET request -> {}", url);
            AuthenticationResponse response = restTemplate.getForObject(url, AuthenticationResponse.class);
            LOGGER.trace(LOG_RECEIVE_RESPONSE, response);
            Optional<AuthenticationResponse> opt = Optional.ofNullable(response);
            return opt.orElseThrow(Exception::new).getToken();
        } catch (Exception e) {
            LOGGER.error("Authentication failed",e);
            throw new RestClientException("Authentication request for '" + username + "' ends up with failure");
        }
    }

    /**
     * Retrieves a list of Sources registered in Analytics.
     *
     * @param authToken
     *          the authentication token
     * @return list of source objects registered in Analytics
     * @throws RestClientException
     *             will be thrown if subscribing ends up with an error
     */

    public List<Source> getSources(String authToken) {
        String url = getHttpUrl().pathSegment(PS_DOMAIN).pathSegment(PS_SOURCES)
                .query("tenant=0").query("locale=en").toUriString();
        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.AUTHORIZATION,authToken);
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        try {
            LOGGER.trace("Executing GET request -> {} with headers {}", url, headers);
            ResponseEntity<List<Source>> responseEntity = restTemplate.exchange(url, HttpMethod.GET,
                    new HttpEntity<>(headers), new ParameterizedTypeReference<List<Source>>() {
                    });
            LOGGER.trace(LOG_RECEIVE_RESPONSE, responseEntity);
            if(responseEntity.getBody() != null) {
                return responseEntity.getBody();
            } else {
                return new ArrayList<Source>();
            }
        } catch (HttpClientErrorException e) {
            LOGGER.error("Unable to retrieve a list of sources",e);
            throw new RestClientException("Unable to retrieve a list of sources");
        }
    }

    /**
     * Retrieves a Source registered in Analytics.
     *
     * @param authToken
     *          the authentication token
     * @param sourceId
     *          the unique identifier of the realtime data source
     * @return a Source object corresponding to the sourceId
     * @throws RestClientException
     *             will be thrown if subscribing ends up with an error
     */

    public Source getSource(String authToken, String sourceId) {
        String url = getHttpUrl().pathSegment(PS_DOMAIN).pathSegment(PS_SOURCES).pathSegment(sourceId)
                .query("tenant=0").query("locale=en").toUriString();
        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.AUTHORIZATION,authToken);
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        try {
            LOGGER.trace("Executing GET request -> {} with headers {}", url, headers);
            ResponseEntity<Source> responseEntity = restTemplate.exchange(url, HttpMethod.GET,
                    new HttpEntity<>(headers), new ParameterizedTypeReference<Source>() {
                    });
            LOGGER.trace(LOG_RECEIVE_RESPONSE, responseEntity);
            if(responseEntity.getBody() != null) {
                return responseEntity.getBody();
            } else {
                return new Source();
            }
        } catch (HttpClientErrorException e) {
            LOGGER.error("Unable to retrieve a list of sources",e);
            throw new RestClientException("Unable to retrieve a list of sources");
        }
    }

    /**
     * Retrieves the list of Producers registered in Analytics.
     *
     * @param authToken
     *          the authentication token
     * @param sourceId
     *          the unique identifier of the realtime data source
     * @return list of Producers registered with the given SourceId
     * @throws RestClientException
     *             will be thrown if subscribing ends up with an error
     */

    public List<Producer> getProducers(String authToken, String sourceId) {
        String url = getHttpUrl().pathSegment(PS_DOMAIN).pathSegment(PS_SOURCES).pathSegment(sourceId)
                .pathSegment(PS_PRODUCERS).query("tenant=0").query("locale=en").toUriString();
        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.AUTHORIZATION,authToken);
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        try {
            LOGGER.trace("Executing GET request -> {} with headers {}", url, headers);
            ResponseEntity<List<Producer>> responseEntity = restTemplate.exchange(url, HttpMethod.GET,
                    new HttpEntity<>(headers), new ParameterizedTypeReference<List<Producer>>() {
                    });
            LOGGER.trace(LOG_RECEIVE_RESPONSE, responseEntity);
            if(responseEntity.getBody() != null) {
                return responseEntity.getBody();
            } else {
                return new ArrayList<Producer>();
            }
        } catch (HttpClientErrorException e) {
            LOGGER.error("Unable to retrieve a list of producers",e);
            throw new RestClientException("Unable to retrieve a list of producers");
        }
    }


    /**
     * Retrieves a Producer registered in Analytics.
     *
     * @param authToken
     *          the authentication token
     * @param sourceId
     *          the unique identifier of the realtime data source
     * @param producerId
     *            the unique identifier of producer
     *
     * @return a Producer for the given sourceId and producerId
     * @throws RestClientException
     *             will be thrown if subscribing ends up with an error     *
     */

    public Producer getProducer(String authToken, String sourceId, String producerId) {
        String url = getHttpUrl().pathSegment(PS_DOMAIN).pathSegment(PS_SOURCES).pathSegment(sourceId)
                .pathSegment(PS_PRODUCERS).pathSegment(producerId).query("tenant=0").query("locale=en").toUriString();
        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.AUTHORIZATION, authToken);
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        try {
            LOGGER.trace("Executing GET request -> {} with headers {}", url, headers);
            ResponseEntity<Producer> responseEntity = restTemplate.exchange(url, HttpMethod.GET,
                    new HttpEntity<>(headers), new ParameterizedTypeReference<Producer>() {
                    });
            LOGGER.trace(LOG_RECEIVE_RESPONSE, responseEntity);
            if (responseEntity.getBody() != null) {
                return responseEntity.getBody();
            } else {
                return new Producer();
            }
        } catch (HttpClientErrorException e) {
            LOGGER.error("Unable to retrieve a list of producers", e);
            throw new RestClientException("Unable to retrieve a list of producers");
        }
    }

    /**
     * Retrieves a Dictionary for a given producderId.
     *
     * @param authToken
     *          the authentication token
     * @param sourceId
     *          the unique identifier of the realtime data source
     * @param producerId
     *            the unique identifier of producer
     * @param locale
     *            the locale. For example {en-us, de, fr, it, es, ko, ja, ru, pt_BR, zh-cn, zh-tw}
     *
     * @return a dictionary for the given sourceId, producerId, and locale
     * @throws RestClientException
     *             will be thrown if subscribing ends up with an error     *
     */

    public Dictionary getDictionary(String authToken, String sourceId, String producerId, String locale) {
        String url = getHttpUrl().pathSegment(PS_DOMAIN).pathSegment(PS_SOURCES).pathSegment(sourceId)
                .pathSegment(PS_PRODUCERS).pathSegment(producerId).pathSegment(PS_DICTIONARIES)
                .query("tenant=0").queryParam("locale", locale).toUriString();

        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.AUTHORIZATION, authToken);
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        try {
            LOGGER.trace("Executing GET request -> {} with headers {}", url, headers);
            ResponseEntity<Dictionary> responseEntity = restTemplate.exchange(url, HttpMethod.GET,
                    new HttpEntity<>(headers), new ParameterizedTypeReference<Dictionary>() {
                    });
            LOGGER.trace(LOG_RECEIVE_RESPONSE, responseEntity);
            if (responseEntity.getBody() != null) {
                return responseEntity.getBody();
            } else {
                return new Dictionary();
            }
        } catch (HttpClientErrorException e) {
            LOGGER.error("Unable to retrieve the dictionary ", e);
            throw new RestClientException("Unable to retrieve the dictionary.");
        }
    }

    /**
     * Retrieves DimensionData
     *
     * @param authToken
     *          the authentication token
     * @param sourceId
     *          the unique identifier of the realtime data source
     * @param dimensionDataName
     *          the name of the dimension-data
     * @return the DimensionData object matching the correspondong sourceId and dimensionDataName.
     *
     * @throws RestClientException
     *             will be thrown if subscribing ends up with an error     *
     */

    public DimensionData getDimensionData(String authToken, String sourceId, String dimensionDataName) {
        String url = getHttpUrl().pathSegment(PS_DOMAIN).pathSegment(PS_SOURCES).pathSegment(sourceId)
                .pathSegment(PS_PRODUCERS).pathSegment(PS_ADMIN_DATA)
                .pathSegment(PS_DIM_DATA).pathSegment(dimensionDataName).pathSegment(PS_DATA_TYPE).pathSegment(dimensionDataName)
                .query("tenant=0").toUriString();

        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.AUTHORIZATION, authToken);
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        try {
            LOGGER.trace("Executing GET request -> {} with headers {}", url, headers);
            ResponseEntity<DimensionData> responseEntity = restTemplate.exchange(url, HttpMethod.GET,
                    new HttpEntity<>(headers), new ParameterizedTypeReference<DimensionData>() {
                    });
            LOGGER.trace(LOG_RECEIVE_RESPONSE, responseEntity);
            if (responseEntity.getBody() != null) {
                return responseEntity.getBody();
            } else {
                return new DimensionData();
            }
        } catch (HttpClientErrorException e) {
            LOGGER.error("Unable to retrieve the dimension data ", e);
            throw new RestClientException("Unable to retrieve the dimension data.");
        }
    }


    /**
     * Performs subscription for realtime data.
     *
     * @param authToken
     *            the authentication token
     * @param request
     *            subscription request
     * @return response with details of subscription
     * @throws RestClientException
     *             will be thrown if subscribing ends up with an error
     */
    public SubscriptionResponse subscribeForRealtimeData(String authToken, SubscriptionRequest request) {
        String url = getHttpUrl().pathSegment(PS_DOMAIN).pathSegment(PS_SOURCES).pathSegment(request.getSourceId()).pathSegment(PS_PRODUCERS)
                .pathSegment(request.getProducerId()).pathSegment(PS_SUBSCRIPTIONS).toUriString();
        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.AUTHORIZATION, authToken);
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        try {
            LOGGER.trace("Executing POST request -> {} with headers {}", url, headers);
            SubscriptionResponse response = restTemplate.postForObject(url, new HttpEntity<>(request, headers),
                    SubscriptionResponse.class);
            LOGGER.trace(LOG_RECEIVE_RESPONSE, response);
            return response;
        } catch (HttpClientErrorException e) {
            LOGGER.error("Unable to perform subscription:",e);
            if (HttpStatus.NOT_FOUND.equals(e.getStatusCode())) {
                throw new RestClientException(
                        "Unable to perform subscription: either sourceId '" + request.getSourceId()
                                + "' either producerId '" + request.getProducerId() + "' of both are invalid");
            } else {
                throw new RestClientException("Unable to perform subscription: " + e.getResponseBodyAsString());
            }
        } catch (HttpServerErrorException | UnknownHttpStatusCodeException e) {
            LOGGER.error("Unable to perform subscription ",e);
            throw new RestClientException("Unable to perform subscription: " + e.getResponseBodyAsString());
        }
    }

    /**
     * Retrieves a list of active subscriptions for specified user.
     *
     * @param authToken
     *          the authentication token
     * @param sourceId
     *          the unique identifier of the realtime data source
     * @return list of subscriptions from response
     * @throws RestClientException
     *             will be thrown if subscribing ends up with an error     *
     */

    public List<SubscriptionResponse> getListOfSubscriptions(String authToken, String sourceId) {
        String url = getHttpUrl().pathSegment(PS_DOMAIN).pathSegment(PS_SOURCES).pathSegment(sourceId)
                .pathSegment(PS_SUBSCRIPTIONS).query("tenant=0").toUriString();

        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.AUTHORIZATION,authToken);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        try {
            LOGGER.trace("Executing GET request -> {} with headers {}", url, headers);
            ResponseEntity<List<SubscriptionResponse>> responseEntity = restTemplate.exchange(url, HttpMethod.GET,
                    new HttpEntity<>(headers), new ParameterizedTypeReference<List<SubscriptionResponse>>() {
                    });
            LOGGER.trace(LOG_RECEIVE_RESPONSE, responseEntity);
            if(responseEntity.getBody() != null) {
                return responseEntity.getBody();
            } else {
                return new ArrayList<SubscriptionResponse>();
            }
        } catch (HttpClientErrorException e) {
            LOGGER.error("Unable to retrieve a list of subscriptions",e);
            throw new RestClientException("Unable to retrieve a list of subscriptions");
        }
    }

    /**
     * Perform unsubscription from realtime data.
     *
     * @param authToken
     *            the authentication token     *
     * @param sourceId
     *            the unique identifier of the realtime data source
     * @param producerId
     *            the unique identifier of producer
     */


    public void unsubscribeFromRealtimeData(String authToken, String sourceId, String producerId) {
        String url = getHttpUrl().pathSegment(PS_DOMAIN).pathSegment(PS_SOURCES).pathSegment(sourceId)
                .pathSegment(PS_PRODUCERS).pathSegment(producerId).pathSegment(PS_SUBSCRIPTIONS).toUriString();

        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.AUTHORIZATION,authToken);
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        try {
            LOGGER.trace("Executing DELETE request -> {} with headers {}", url, headers);
            restTemplate.exchange(url, HttpMethod.DELETE,
                    new HttpEntity<>(headers), new ParameterizedTypeReference<Void>() {
                    });
        } catch (HttpClientErrorException e) {
            LOGGER.error("Unable to perform unsubscription",e);
            throw new RestClientException(
                    "Unable to perform unsubscription: either sourceId '" + sourceId
                            + "' either producerId '" + producerId + "' of both are invalid");
        }
    }

    private UriComponentsBuilder getHttpUrl() {
        return UriComponentsBuilder.newInstance().scheme(SCHEME_HTTPS).host(hostname).port(port);
    }

}
