/*
 * Copyright (c) 2013, 2020 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.message.internal;

import java.lang.reflect.Method;
import java.util.logging.Logger;

import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;

import jakarta.annotation.Priority;

import org.glassfish.jersey.internal.PropertiesDelegate;

/**
 * Low level Tracing support API.
 *
 * Use one instance per request.
 *
 * @author Libor Kramolis
 * @since 2.3
 */
public abstract class TracingLogger {

    /**
     * {@code TracingLogger} instance is placed in request context properties under this name.
     */
    public static final String PROPERTY_NAME = TracingLogger.class.getName();
    /**
     * HTTP header prefix.
     */
    private static final String HEADER_TRACING_PREFIX = "X-Jersey-Tracing-";
    /**
     * Request header name to change application default tracing level.
     */
    public static final String HEADER_THRESHOLD = HEADER_TRACING_PREFIX + "Threshold";
    /**
     * Request header name to switch on request tracing.
     * Make sense in case of tracing support enabled by ON_DEMAND value.
     */
    public static final String HEADER_ACCEPT = HEADER_TRACING_PREFIX + "Accept";
    /**
     * Request header name to set JDK logger name suffix to identify a request logs.
     */
    public static final String HEADER_LOGGER = HEADER_TRACING_PREFIX + "Logger";
    /**
     * Response header name format.
     */
    private static final String HEADER_RESPONSE_FORMAT = HEADER_TRACING_PREFIX + "%03d";
    /**
     * Default event level.
     */
    public static final Level DEFAULT_LEVEL = Level.TRACE;
    /**
     * JDK logger name prefix.
     */
    private static final String TRACING_LOGGER_NAME_PREFIX = "org.glassfish.jersey.tracing";
    /**
     * Default JDK logger name suffix. This can be overwrite by header {@link #HEADER_LOGGER}.
     */
    private static final String DEFAULT_LOGGER_NAME_SUFFIX = "general";
    /**
     * Empty (no-op) tracing logger.
     */
    private static final TracingLogger EMPTY = new TracingLogger() {

        @Override
        public boolean isLogEnabled(final Event event) {
            return false;
        }

        @Override
        public void log(final Event event, final Object... args) {
            // no-op
        }

        @Override
        public void logDuration(final Event event, final long fromTimestamp, final Object... args) {
            // no-op
        }

        @Override
        public long timestamp(final Event event) {
            return -1;
        }

        @Override
        public void flush(final MultivaluedMap<String, Object> headers) {
            // no-op
        }
    };

    /**
     * Returns instance of {@code TracingLogger} associated with current request processing
     * ({@code propertiesDelegate}).
     *
     * @param propertiesDelegate request associated runtime properties. Can be {@code null} if not running on server side.
     * @return returns instance of {@code TracingLogger} from {@code propertiesDelegate}. Does not return {@code null}.
     */
    // TODO look for places where getInstance(RequestProcessingContext) would make sense
    public static TracingLogger getInstance(final PropertiesDelegate propertiesDelegate) {
        if (propertiesDelegate == null) {
            //not server side
            return EMPTY;
        }
        final TracingLogger tracingLogger = (TracingLogger) propertiesDelegate.getProperty(PROPERTY_NAME);
        return (tracingLogger != null) ? tracingLogger : EMPTY;
    }

    /**
     * Create new Tracing logger.
     *
     * @param threshold        tracing level threshold.
     * @param loggerNameSuffix tracing logger name suffix.
     * @return new tracing logger.
     */
    public static TracingLogger create(final Level threshold, final String loggerNameSuffix) {
        return new TracingLoggerImpl(threshold, loggerNameSuffix);
    }

    /**
     * Get an empty (no-op) tracing logger instance.
     *
     * @return empty tracing logger instance.
     */
    public static TracingLogger empty() {
        return EMPTY;
    }

    /**
     * Test if a tracing support is enabled (according to {@code propertiesDelegate} setting) and
     * if {@code event} can be logged (according to {@code event.level} and threshold level set).
     *
     * @param event event type to be tested
     * @return {@code true} if {@code event} can be logged
     */
    public abstract boolean isLogEnabled(Event event);

    /**
     * Try to log event according to event level and request context threshold level setting.
     *
     * @param event event type to be logged
     * @param args  message arguments (in relation to {@link org.glassfish.jersey.message.internal.TracingLogger
     * .Event#messageFormat()}
     */
    public abstract void log(Event event, Object... args);

    /**
     * Try to log event according to event level and request context threshold level setting.
     *
     * If logging support is switched on for current request and event setting the method computes duration of event and log
     * message. If {@code fromTimestamp} is not set (i.e. {@code -1}) then duration of event
     * is {@code 0}.
     *
     * @param event         event type to be logged
     * @param fromTimestamp logged event is running from the timestamp in nanos. {@code -1} in case event has no duration
     * @param args          message arguments (in relation to {@link org.glassfish.jersey.message.internal.TracingLogger
     * .Event#messageFormat()}
     */
    public abstract void logDuration(Event event, long fromTimestamp, Object... args);

    /**
     * If logging support is switched on for current request and event setting the method returns current timestamp in nanos.
     *
     * @param event event type to be logged
     * @return Current timestamp in nanos or {@code -1} if tracing is not enabled
     */
    public abstract long timestamp(Event event);

    /**
     * Stores collected tracing messages to response HTTP header.
     *
     * @param headers message headers.
     */
    public abstract void flush(MultivaluedMap<String, Object> headers);

    /**
     * Real implementation of tracing logger.
     */
    private static final class TracingLoggerImpl extends TracingLogger {

        private final Logger logger;
        private final Level threshold;
        private final TracingInfo tracingInfo;

        public TracingLoggerImpl(final Level threshold, String loggerNameSuffix) {
            this.threshold = threshold;

            this.tracingInfo = new TracingInfo();

            loggerNameSuffix = loggerNameSuffix != null ? loggerNameSuffix : DEFAULT_LOGGER_NAME_SUFFIX;
            this.logger = Logger.getLogger(TRACING_LOGGER_NAME_PREFIX + "." + loggerNameSuffix);
        }

        @Override
        public boolean isLogEnabled(final Event event) {
            return isEnabled(event.level());
        }

        @Override
        public void log(final Event event, final Object... args) {
            logDuration(event, -1, args);
        }

        @Override
        public void logDuration(final Event event, final long fromTimestamp, final Object... args) {
            if (isEnabled(event.level())) {
                final long toTimestamp;
                if (fromTimestamp == -1) {
                    toTimestamp = -1;
                } else {
                    toTimestamp = System.nanoTime();
                }
                long duration = 0;
                if ((fromTimestamp != -1) && (toTimestamp != -1)) {
                    duration = toTimestamp - fromTimestamp;
                }
                logImpl(event, duration, args);
            }
        }

        @Override
        public long timestamp(final Event event) {
            if (isEnabled(event.level())) {
                return System.nanoTime();
            }
            return -1;
        }

        @Override
        public void flush(final MultivaluedMap<String, Object> headers) {
            final String[] messages = tracingInfo.getMessages();
            for (int i = 0; i < messages.length; i++) {
                headers.putSingle(String.format(TracingLogger.HEADER_RESPONSE_FORMAT, i), messages[i]);
            }
        }

        /**
         * Log message for specified event type.
         *
         * The event contains name, category, level and also message format.
         * Message format will be formatted by parameter {@code messageArgs} is used to format event.
         * If there is no message format then each message arg is separated by space.
         * Final message also contains event name (JDK Log) or category (HTTP header) and time stamp.
         *
         * @param event       Event type of log
         * @param duration    Time duration of logged event. Can be {@code 0}.
         * @param messageArgs message arguments
         */
        private void logImpl(final Event event, final long duration, final Object... messageArgs) {
            if (isEnabled(event.level())) {
                final String[] messageArgsStr = new String[messageArgs.length];
                for (int i = 0; i < messageArgs.length; i++) {
                    messageArgsStr[i] = formatInstance(messageArgs[i]);
                }
                final TracingInfo.Message message = new TracingInfo.Message(event, duration, messageArgsStr);
                tracingInfo.addMessage(message);

                final java.util.logging.Level loggingLevel;
                switch (event.level()) {
                    case SUMMARY:
                        loggingLevel = java.util.logging.Level.FINE;
                        break;
                    case TRACE:
                        loggingLevel = java.util.logging.Level.FINER;
                        break;
                    case VERBOSE:
                        loggingLevel = java.util.logging.Level.FINEST;
                        break;
                    default:
                        loggingLevel = java.util.logging.Level.OFF;
                }
                if (logger.isLoggable(loggingLevel)) {
                    logger.log(loggingLevel,
                            event.name() + ' ' + message.toString() + " [" + TracingInfo.formatDuration(duration) + " ms]");
                }
            }
        }

        private boolean isEnabled(final Level level) {
            return threshold.ordinal() >= level.ordinal();
        }

        /**
         * Format info of instance.
         *
         * It shows its class name, identity hash code, and following info if available: priority value, response detail.
         *
         * @param instance instance to be formatted
         * @return Formatted info of instance.
         */
        private static String formatInstance(final Object instance) {
            final StringBuilder textSB = new StringBuilder();
            if (instance == null) {
                textSB.append("null");
            } else if ((instance instanceof Number) || (instance instanceof String) || (instance instanceof Method)) {
                textSB.append(instance.toString());
            } else if (instance instanceof Response.StatusType) {
                textSB.append(formatStatusInfo((Response.StatusType) instance));
            } else {
                textSB.append('[');
                formatInstance(instance, textSB);
                if (instance.getClass().isAnnotationPresent(Priority.class)) {
                    textSB.append(" #").append(instance.getClass().getAnnotation(Priority.class).value());
                }
                if (instance instanceof WebApplicationException) {
                    formatResponse(((WebApplicationException) instance).getResponse(), textSB);
                } else if (instance instanceof Response) {
                    formatResponse(((Response) instance), textSB);
                }
                textSB.append(']');
            }
            return textSB.toString();
        }

        /**
         * Basic format of instance - just class name and identity hash code.
         *
         * @param instance instance to be formatted
         * @param textSB   Formatted info will be appended to {@code StringBuilder}
         */
        private static void formatInstance(final Object instance, final StringBuilder textSB) {
            textSB.append(instance.getClass().getName()).append(" @")
                    .append(Integer.toHexString(System.identityHashCode(instance)));
        }

        /**
         * Format of response - status code, status family, reason phrase and info about entity.
         *
         * @param response response to be formatted
         * @param textSB   Formatted info will be appended to {@code StringBuilder}
         */
        private static void formatResponse(final Response response, final StringBuilder textSB) {
            textSB.append(" <").append(formatStatusInfo(response.getStatusInfo())).append('|');
            if (response.hasEntity()) {
                formatInstance(response.getEntity(), textSB);
            } else {
                textSB.append("-no-entity-");
            }
            textSB.append('>');
        }

        private static String formatStatusInfo(final Response.StatusType statusInfo) {
            return String.valueOf(statusInfo.getStatusCode()) + '/' + statusInfo.getFamily() + '|' + statusInfo.getReasonPhrase();
        }
    }

    /**
     * Level of tracing message.
     */
    public static enum Level {
        /**
         * Brief tracing information level.
         */
        SUMMARY,
        /**
         * Detailed tracing information level.
         */
        TRACE,
        /**
         * Extremely detailed tracing information level.
         */
        VERBOSE
    }

    /**
     * Type of event.
     */
    public static interface Event {

        /**
         * Name of event, should be unique.
         * Is logged by JDK logger.
         *
         * @return event name.
         */
        public String name();

        /**
         * Category of event, more events share same category.
         * Is used to format response HTTP header.
         *
         * @return event category.
         */
        public String category();

        /**
         * Level of event.
         * Is used to check if the event is logged according to application/request settings.
         *
         * @return event trace level.
         */
        public Level level();

        /**
         * Message format. Use {@link String#format(String, Object...)} format.
         * Can be null. In that case message arguments are separated by space.
         *
         * @return message format
         */
        public String messageFormat();
    }

}
