/*
 * Copyright (c) 2017-2020 Contributors to the Eclipse Foundation
 *
 * See the NOTICES file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 */

package org.eclipse.microprofile.health;

import org.eclipse.microprofile.health.spi.HealthCheckResponseProvider;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * The response to a health check invocation.
 * <p>
 * The {@link HealthCheckResponse} class is reserved for an extension by implementation providers.
 * An application should use one of the static methods to create a Response instance using a 
 * {@link HealthCheckResponseBuilder}.
 * When used on the consuming end, The class can also be instantiated directly.
 * </p>
 */
public class HealthCheckResponse {

    private static final Logger LOGGER = Logger.getLogger(HealthCheckResponse.class.getName());

    private static volatile HealthCheckResponseProvider provider = null;

    private final String name;

    private final Status status;

    private final Optional<Map<String, Object>> data;

    /**
     * Constructor allowing instantiation from 3rd party framework like MicroProfile Rest client
     * @param name Health Check procedure's name
     * @param status Health Check procedure's status
     * @param data additional data for Health Check procedure
     */
    public HealthCheckResponse(String name, Status status, Optional<Map<String, Object>> data) {
        this.name = name;
        this.status = status;
        this.data = data;
    }

    /**
     * Default constructor
     */
    public HealthCheckResponse() {
        name = null;
        status = null;
        data = null;
    }

    /**
     * Used by OSGi environment where the service loader pattern is not supported.
     *
     * @param provider the provider instance to use.
     */
    public static void setResponseProvider(HealthCheckResponseProvider provider) {
        HealthCheckResponse.provider = provider;
    }

    /**
     * Creates a {@link HealthCheckResponseBuilder} with a name.
     *
     * @param name the check name
     * @return a new health check builder with a name
     */
    public static HealthCheckResponseBuilder named(String name) {

        return getProvider().createResponseBuilder().name(name);
    }

    /**
     * Creates an empty {@link HealthCheckResponseBuilder}.
     *
     * <b>Note:</b> The health check response name is required and needs to be set before the response is constructed.
     *
     * @return a new, empty health check builder
     */
    public static HealthCheckResponseBuilder builder() {
        return getProvider().createResponseBuilder();
    }

    /**
     * Creates a successful health check with a name.
     * 
     * @param name the check name
     * @return a new sucessful health check response with a name
     */
    public static HealthCheckResponse up(String name) {
        return HealthCheckResponse.named(name).up().build();
    }

    /**
     * Creates a failed health check with a name.
     * 
     * @param name the check name
     * @return a new failed health check response with a name
     */
    public static HealthCheckResponse down(String name) {
        return HealthCheckResponse.named(name).down().build();
    }

    private static HealthCheckResponseProvider getProvider() {
        if (provider == null) {
            synchronized (HealthCheckResponse.class) {
                if (provider != null) {
                    return provider;
                }

                HealthCheckResponseProvider newInstance = find(HealthCheckResponseProvider.class);

                if (newInstance == null) {
                    throw new IllegalStateException("No HealthCheckResponseProvider implementation found!");
                }

                provider = newInstance;
            }
        }
        return provider;
    }

    // the actual contract

    public enum Status {UP, DOWN}

    public String getName(){
        return name;
    }

    public Status getStatus(){
        return status;
    }

    public Optional<Map<String, Object>> getData() {
        return data;
    }

    private static <T> T find(Class<T> service) {

        T serviceInstance = find(service, HealthCheckResponse.getContextClassLoader());

        // alternate classloader
        if (null == serviceInstance) {
            serviceInstance = find(service, HealthCheckResponse.class.getClassLoader());
        }

        // service cannot be found
        if (null == serviceInstance) {
            throw new IllegalStateException("Unable to find service " + service.getName());
        }

        return serviceInstance;
    }

    private static <T> T find(Class<T> service, ClassLoader cl) {

        T serviceInstance = null;

        try {
            ServiceLoader<T> services = ServiceLoader.load(service, cl);

            for (T spi : services) {
                if (serviceInstance != null) {
                    throw new IllegalStateException(
                            "Multiple service implementations found: "
                                    + spi.getClass().getName() + " and "
                                    + serviceInstance.getClass().getName());
                }
                serviceInstance = spi;
            }
        }
        catch (Throwable t) {
            LOGGER.log(Level.SEVERE, "Error loading service " + service.getName() + ".", t);
        }

        return serviceInstance;
    }


    private static ClassLoader getContextClassLoader() {
        return AccessController.doPrivileged((PrivilegedAction<ClassLoader>) () -> {
            ClassLoader cl = null;
            try {
                cl = Thread.currentThread().getContextClassLoader();
            }
            catch (SecurityException ex) {
                LOGGER.log(
                        Level.WARNING,
                        "Unable to get context classloader instance.",
                        ex);
            }
            return cl;
        });
    }
}