package org.jboss.resteasy.tracing;

import jakarta.ws.rs.core.Configuration;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.SecurityContext;

import org.jboss.resteasy.core.ResteasyContext;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.tracing.api.RESTEasyTracing;
import org.jboss.resteasy.tracing.api.RESTEasyTracingConfig;
import org.jboss.resteasy.tracing.api.RESTEasyTracingLevel;

public interface RESTEasyTracingLogger {
    class TRACING {
        public static final boolean AVAILABLE;
        static {
            boolean b;
            try {
                Class.forName("org.jboss.resteasy.tracing.api.RESTEasyTracing");
                b = true;
            } catch (Throwable t) {
                b = false;
            }
            AVAILABLE = b;
        }
    }

    boolean isLogEnabled(String eventName);

    void log(String eventName, Object... args);

    void logDuration(String eventName, long fromTimestamp, Object... args);

    long timestamp(String eventName);

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

    /**
     * Create new Tracing logger.
     *
     * @param requestId        request id.
     * @param threshold        tracing level threshold.
     * @param loggerNameSuffix tracing logger name suffix.
     * @return new tracing logger.
     */
    static RESTEasyTracingLogger create(final String requestId, final String threshold, final String loggerNameSuffix) {
        return create(requestId, threshold, loggerNameSuffix, null);
    }

    static RESTEasyTracingLogger create(String requestId, String tracingThreshold, String tracingLoggerNameSuffix,
            String tracingInfoFormat) {
        if (!TRACING.AVAILABLE) {
            return EMPTY;
        }
        return new RESTEasyTracingLoggerImpl(requestId, RESTEasyTracingLevel.valueOf(tracingThreshold), tracingLoggerNameSuffix,
                tracingInfoFormat);

    }

    /**
     * Create new Tracing logger.
     * Used by client invocation.
     *
     * @param invocationId     calling ClientInvocation instance id
     * @param configuration    configuration
     * @param loggerNameSuffix tracing logger name suffix.
     * @return new tracing logger.
     */
    static RESTEasyTracingLogger create(final String invocationId, final Configuration configuration,
            final String loggerNameSuffix) {
        if (!TRACING.AVAILABLE) {
            return EMPTY;
        }
        return new RESTEasyTracingLoggerImpl(invocationId, RESTEasyTracingUtils.getRESTEasyTracingThreshold(configuration),
                loggerNameSuffix);
    }

    /**
     * Returns instance of {@code TracingLogger} associated with current request processing
     * ({@code propertiesDelegate}).
     *
     * @param request http request
     * @return returns instance of {@code TracingLogger} from {@code ResteasyProviderFactory}. Does not return {@code null}.
     */
    static RESTEasyTracingLogger getInstance(HttpRequest request) {
        if (request == null || !TRACING.AVAILABLE) {
            return EMPTY;
        }

        final RESTEasyTracingLogger tracingLogger = (RESTEasyTracingLogger) request.getAttribute(RESTEasyTracing.PROPERTY_NAME);

        return tracingLogger == null ? EMPTY : tracingLogger;
    }

    static RESTEasyTracingLogger empty() {
        return EMPTY;
    }

    RESTEasyTracingLogger EMPTY = new RESTEasyTracingLogger() {

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

        @Override
        public boolean isLogEnabled(String eventName) {
            return false;
        }

        @Override
        public void log(String eventName, Object... args) {
            // no-op
        }

        @Override
        public void logDuration(String eventName, long fromTimestamp, Object... args) {
            // no-op
        }

        @Override
        public long timestamp(String eventName) {
            return 0;
        }
    };

    /**
     * According to configuration/request header it initialize {@link RESTEasyTracingLogger} and put it to the request
     * properties.
     *
     * @param configuration application-wide tracing configuration type and tracing level threshold.
     * @param request       request instance to get runtime properties to store {@link RESTEasyTracingLogger} instance to
     *                      if tracing support is enabled for the request.
     */
    static void initTracingSupport(Configuration configuration,
            HttpRequest request) {
        if (!TRACING.AVAILABLE || request.getAttribute(RESTEasyTracing.PROPERTY_NAME) != null)
            return;

        final RESTEasyTracingLogger tracingLogger;
        if (RESTEasyTracingUtils.isTracingSupportEnabled(RESTEasyTracingUtils.getRESTEasyTracingConfig(configuration),
                request)) {
            tracingLogger = RESTEasyTracingLogger.create(request.toString(),
                    RESTEasyTracingUtils.getTracingThreshold(RESTEasyTracingUtils.getRESTEasyTracingThreshold(configuration),
                            request),
                    RESTEasyTracingUtils.getTracingLoggerNameSuffix(request),
                    RESTEasyTracingUtils.getTracingInfoFormat(request));
        } else {
            tracingLogger = RESTEasyTracingLogger.empty();
        }

        request.setAttribute(RESTEasyTracing.PROPERTY_NAME, tracingLogger);

    }

    /**
     * Log tracing messages START events.
     *
     * @param request container request instance to get runtime properties
     *                to check if tracing support is enabled for the request.
     */
    static void logStart(HttpRequest request) {
        if (!TRACING.AVAILABLE || request == null) {
            return;
        }

        RESTEasyTracingLogger tracingLogger = RESTEasyTracingLogger.getInstance(request);
        if (tracingLogger.isLogEnabled("START")) {
            StringBuilder text = new StringBuilder();
            SecurityContext securityContext = ResteasyContext.getContextData(SecurityContext.class);
            text.append(String.format("baseUri=[%s] requestUri=[%s] method=[%s] authScheme=[%s]",
                    request.getUri().getBaseUri(), request.getUri().getRequestUri(), request.getHttpMethod(),
                    RESTEasyTracingUtils
                            .toStringOrNA(securityContext == null ? null : securityContext.getAuthenticationScheme())));
            for (String header : RESTEasyTracingUtils.SUMMARY_HEADERS) {
                text.append(String.format(" %s=%s", header,
                        RESTEasyTracingUtils.toStringOrNA(RESTEasyTracingUtils.getHeaderString(request, header))));
            }
            tracingLogger.log("START", text.toString());
        }
        if (tracingLogger.isLogEnabled("START_HEADERS")) {
            StringBuilder text = new StringBuilder();
            HttpHeaders headers = request.getHttpHeaders();
            if (headers != null) {
                for (String header : headers.getRequestHeaders().keySet()) {
                    if (!RESTEasyTracingUtils.SUMMARY_HEADERS.contains(header)) {
                        text.append(String.format(" %s=%s", header,
                                RESTEasyTracingUtils.toStringOrNA(headers.getRequestHeaders().get(header))));
                    }
                }
                if (text.length() > 0) {
                    text.insert(0, "Other request headers:");
                }
                tracingLogger.log("START_HEADERS", text.toString());
            }
        }
    }

    static boolean isTracingConfigALL(Configuration configuration) {
        return TRACING.AVAILABLE && RESTEasyTracingUtils.getRESTEasyTracingConfig(configuration) == RESTEasyTracingConfig.ALL;
    }

    /**
     * Return configuration type of tracing support according to application configuration.
     * <p>
     * By default tracing support is switched OFF.
     *
     * @param configuration configuration
     * @return the configuration type
     */
    static String getTracingConfig(Configuration configuration) {
        return TRACING.AVAILABLE ? RESTEasyTracingUtils.getRESTEasyTracingConfig(configuration).toString() : null;
    }

    /**
     * Get application-wide tracing level threshold.
     *
     * @param configuration configuration
     * @return tracing level threshold.
     */
    static String getTracingThreshold(Configuration configuration) {
        return TRACING.AVAILABLE ? RESTEasyTracingUtils.getRESTEasyTracingThreshold(configuration).toString() : null;
    }
}
