/*
 * This program and the accompanying materials are made available under the terms of the
 * Eclipse Public License v2.0 which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-v20.html
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Copyright Contributors to the Zowe Project.
 */
package org.zowe.apiml.apicatalog.instance;

import org.zowe.apiml.apicatalog.discovery.DiscoveryConfigProperties;
import org.zowe.apiml.message.log.ApimlLogger;
import org.zowe.apiml.product.instance.InstanceInitializationException;
import org.zowe.apiml.product.logging.annotations.InjectApimlLogger;
import org.zowe.apiml.product.registry.ApplicationWrapper;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.converters.jackson.EurekaJsonJacksonCodec;
import com.netflix.discovery.shared.Applications;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import javax.validation.constraints.NotBlank;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;

/**
 * Service for instance retrieval from Eureka
 */
@Slf4j
@Service
public class InstanceRetrievalService {

    private final DiscoveryConfigProperties discoveryConfigProperties;
    private final RestTemplate restTemplate;

    private static final String APPS_ENDPOINT = "apps/";
    private static final String DELTA_ENDPOINT = "delta";
    private static final String UNKNOWN = "unknown";

    @InjectApimlLogger
    private final ApimlLogger apimlLog = ApimlLogger.empty();

    @Autowired
    public InstanceRetrievalService(DiscoveryConfigProperties discoveryConfigProperties,
                                    RestTemplate restTemplate) {
        this.discoveryConfigProperties = discoveryConfigProperties;
        this.restTemplate = restTemplate;

        configureUnicode(restTemplate);
    }

    /**
     * Retrieves {@link InstanceInfo} of particular service
     *
     * @param serviceId the service to search for
     * @return service instance
     */
    public InstanceInfo getInstanceInfo(@NotBlank(message = "Service Id must be supplied") String serviceId) {
        if (serviceId.equalsIgnoreCase(UNKNOWN)) {
            return null;
        }

        InstanceInfo instanceInfo = null;
        try {
            Pair<String, Pair<String, String>> requestInfo = constructServiceInfoQueryRequest(serviceId, false);

            // call Eureka REST endpoint to fetch single or all Instances
            ResponseEntity<String> response = queryDiscoveryForInstances(requestInfo);
            if (response.getStatusCode().is2xxSuccessful()) {
                instanceInfo = extractSingleInstanceFromApplication(serviceId, requestInfo.getLeft(), response);
            }
        } catch (Exception e) {
            String msg = "An error occurred when trying to get instance info for:  " + serviceId;
            log.debug(msg, e.getMessage());
            throw new InstanceInitializationException(msg);
        }

        return instanceInfo;
    }

    /**
     * Retrieve instances from the discovery service
     *
     * @param delta filter the registry information to the just updated infos
     * @return the Applications object that wraps all the registry information
     */
    public Applications getAllInstancesFromDiscovery(boolean delta) {

        Pair<String, Pair<String, String>> requestInfo = constructServiceInfoQueryRequest(null, delta);

        //  call Eureka REST endpoint to fetch single or all Instances
        ResponseEntity<String> response = queryDiscoveryForInstances(requestInfo);

        return extractApplications(requestInfo, response);
    }

    /**
     * Parse information from the response and extract the Applications object which contains all the registry information returned by eureka server
     *
     * @param requestInfo contains the pair of discovery URL and discovery credentials (for HTTP access)
     * @param response    the http response
     * @return Applications object that wraps all the registry information
     */
    private Applications extractApplications(Pair<String, Pair<String, String>> requestInfo, ResponseEntity<String> response) {
        Applications applications = null;
        if (!HttpStatus.OK.equals(response.getStatusCode()) || response.getBody() == null) {
            apimlLog.log("org.zowe.apiml.apicatalog.serviceRetrievalRequestFailed", response.getStatusCode(), response.getStatusCode().getReasonPhrase(), requestInfo.getLeft());
        } else {
            ObjectMapper mapper = new EurekaJsonJacksonCodec().getObjectMapper(Applications.class);
            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
            try {
                applications = mapper.readValue(response.getBody(), Applications.class);
            } catch (IOException e) {
                apimlLog.log("org.zowe.apiml.apicatalog.serviceRetrievalParsingFailed", e.getMessage());
            }
        }
        return applications;
    }

    /**
     * Query Discovery
     *
     * @param requestInfo information used to query the discovery service
     * @return ResponseEntity<String> query response
     */
    private ResponseEntity<String> queryDiscoveryForInstances(Pair<String, Pair<String, String>> requestInfo) {
        HttpEntity<?> entity = new HttpEntity<>(null, createRequestHeader(requestInfo.getRight()));
        ResponseEntity<String> response = restTemplate.exchange(
            requestInfo.getLeft(),
            HttpMethod.GET,
            entity,
            String.class);
        if (!response.getStatusCode().is2xxSuccessful()) {
            log.debug("Could not locate instance for request: " + requestInfo.getLeft()
                + ", " + response.getStatusCode() + " = " + response.getStatusCode().getReasonPhrase());
        }
        return response;
    }

    /**
     * @param serviceId the service to search for
     * @param url       try to find instance with this discovery url
     * @param response  the fetch attempt response
     * @return service instance
     */
    private InstanceInfo extractSingleInstanceFromApplication(String serviceId, String url, ResponseEntity<String> response) {
        ApplicationWrapper application = null;
        if (!HttpStatus.OK.equals(response.getStatusCode()) || response.getBody() == null) {
            log.debug("Could not retrieve service: " + serviceId + " instance info from discovery --" + response.getStatusCode()
                + " -- " + response.getStatusCode().getReasonPhrase() + " -- URL: " + url);
            return null;
        } else {
            ObjectMapper mapper = new ObjectMapper();
            mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
            try {
                application = mapper.readValue(response.getBody(), ApplicationWrapper.class);
            } catch (IOException e) {
                log.debug("Could not extract service: " + serviceId + " info from discovery --" + e.getMessage(), e);
            }
        }

        if (application != null
            && application.getApplication() != null
            && application.getApplication().getInstances() != null
            && !application.getApplication().getInstances().isEmpty()) {
            return application.getApplication().getInstances().get(0);
        } else {
            return null;
        }
    }

    /**
     * Construct a tuple used to query the discovery service
     *
     * @param serviceId optional service id
     * @return request information
     */
    private Pair<String, Pair<String, String>> constructServiceInfoQueryRequest(String serviceId, boolean getDelta) {
        String discoveryServiceLocatorUrl = discoveryConfigProperties.getLocations() + APPS_ENDPOINT;
        if (getDelta) {
            discoveryServiceLocatorUrl += DELTA_ENDPOINT;
        } else {
            if (serviceId != null) {
                discoveryServiceLocatorUrl += serviceId.toLowerCase();
            }
        }

        String eurekaUsername = discoveryConfigProperties.getEurekaUserName();
        String eurekaUserPassword = discoveryConfigProperties.getEurekaUserPassword();

        Pair<String, String> discoveryServiceCredentials = Pair.of(eurekaUsername, eurekaUserPassword);

        log.debug("Eureka credentials retrieved for user: {} {}",
            eurekaUsername,
            (!eurekaUserPassword.isEmpty() ? "*******" : "NO PASSWORD")
        );

        log.debug("Checking instance info from: " + discoveryServiceLocatorUrl);
        return Pair.of(discoveryServiceLocatorUrl, discoveryServiceCredentials);
    }

    /**
     * Create HTTP headers
     *
     * @return HTTP Headers
     */
    private HttpHeaders createRequestHeader(Pair<String, String> credentials) {
        HttpHeaders headers = new HttpHeaders();
        if (credentials != null && credentials.getLeft() != null && credentials.getRight() != null) {
            String basicToken = "Basic " + Base64.getEncoder().encodeToString((credentials.getLeft() + ":"
                + credentials.getRight()).getBytes());
            headers.add("Authorization", basicToken);
        }
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setAccept(new ArrayList<>(Collections.singletonList(MediaType.APPLICATION_JSON)));
        return headers;
    }

    private void configureUnicode(RestTemplate restTemplate) {
        restTemplate.getMessageConverters()
            .add(0, new StringHttpMessageConverter(Charset.forName("UTF-8")));
    }
}