package org.jboss.resteasy.tracing;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

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

import org.jboss.logging.Logger;
import org.jboss.resteasy.tracing.api.RESTEasyMsgTraceEvent;
import org.jboss.resteasy.tracing.api.RESTEasyServerTracingEvent;
import org.jboss.resteasy.tracing.api.RESTEasyTracing;
import org.jboss.resteasy.tracing.api.RESTEasyTracingEvent;
import org.jboss.resteasy.tracing.api.RESTEasyTracingInfo;
import org.jboss.resteasy.tracing.api.RESTEasyTracingLevel;
import org.jboss.resteasy.tracing.api.RESTEasyTracingMessage;

class RESTEasyTracingLoggerImpl extends RESTEasyTracing implements RESTEasyTracingLogger {

    private final Logger logger;
    private final RESTEasyTracingLevel threshold;
    private final RESTEasyTracingInfo tracingInfo;
    private final String requestId;
    private static final Map<String, RESTEasyTracingEvent> string2event = new HashMap<>();

    static {
        for (RESTEasyTracingEvent v : RESTEasyServerTracingEvent.values()) {
            string2event.put(v.name(), v);
        }
        for (RESTEasyTracingEvent v : RESTEasyMsgTraceEvent.values()) {
            string2event.put(v.name(), v);
        }
    }

    RESTEasyTracingLoggerImpl(final String requestId, final RESTEasyTracingLevel threshold, final String loggerNameSuffix) {
        this(requestId, threshold, loggerNameSuffix, null);
    }

    RESTEasyTracingLoggerImpl(final String requestId, final RESTEasyTracingLevel threshold, final String loggerNameSuffix,
            final String format) {
        this.requestId = requestId;
        this.threshold = threshold;
        if (loggerNameSuffix != null) {
            this.logger = Logger.getLogger(TRACING_LOGGER_NAME_PREFIX + "." + loggerNameSuffix);
        } else {
            this.logger = Logger.getLogger(TRACING_LOGGER_NAME_PREFIX + "." + DEFAULT_LOGGER_NAME_SUFFIX);
        }
        tracingInfo = RESTEasyTracingInfo.get(format);
    }

    private RESTEasyTracingEvent getEvent(String eventName) {
        RESTEasyTracingEvent e = string2event.get(eventName);
        if (e == null) {
            throw new IllegalArgumentException();
        }
        return e;
    }

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

    @Override
    public void log(String eventName, Object... args) {
        log(getEvent(eventName), args);
    }

    @Override
    public void logDuration(String eventName, long fromTimestamp, Object... args) {
        logDuration(getEvent(eventName), fromTimestamp, args);
    }

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

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

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

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

    @Override
    public void logDuration(final RESTEasyTracingEvent 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);
        }
    }

    private void logImpl(final RESTEasyTracingEvent 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 RESTEasyTracingMessage message = new RESTEasyTracingMessage(event, requestId, duration, messageArgsStr);
            tracingInfo.addMessage(message);

            final Logger.Level loggingLevel;
            switch (event.level()) {
                case SUMMARY:
                    loggingLevel = Logger.Level.INFO;
                    break;
                case TRACE:
                    loggingLevel = Logger.Level.DEBUG;
                    break;
                case VERBOSE:
                    loggingLevel = Logger.Level.TRACE;
                    break;
                default:
                    loggingLevel = Logger.Level.INFO;
            }
            if (logger.isEnabled(loggingLevel)) {
                logger.log(loggingLevel,
                        new StringBuilder()
                                .append(requestId)
                                .append(' ')
                                .append(event.name())
                                .append(' ')
                                .append(message.toString())
                                .append(" [")
                                .append(tracingInfo.formatDuration(duration))
                                .append(" ms]")
                                .toString());
            }
        }
    }

    private static void formatInstance(final Object instance, final StringBuilder text) {
        text.append(instance.getClass().getName()).append(" @")
                .append(Integer.toHexString(System.identityHashCode(instance)));
    }

    private static void formatResponse(final Response response, final StringBuilder text) {
        text.append(" <").append(formatStatusInfo(response.getStatusInfo())).append('|');

        Object entity;
        try {
            entity = response.getEntity();
        } catch (IllegalStateException ise) {
            // [RESTEASY-1142] IllegalStateException from ClientResponse.getEntity() means the response is closed and got no entity
            entity = null;
        }

        if (entity != null) {
            formatInstance(entity, text);
        } else {
            text.append("-no-entity-");
        }

        text.append('>');
    }

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

    private String formatInstance(Object instance) {
        final StringBuilder text = new StringBuilder();
        if (instance == null) {
            text.append("null");
        } else if ((instance instanceof Number) || (instance instanceof String) || (instance instanceof Method)) {
            text.append(instance.toString());
        } else if (instance instanceof Response.StatusType) {
            text.append(formatStatusInfo((Response.StatusType) instance));
        } else {
            text.append('[');
            formatInstance(instance, text);
            if (instance.getClass().isAnnotationPresent(Priority.class)) {
                text.append(" #").append(instance.getClass().getAnnotation(Priority.class).value());
            }
            if (instance instanceof WebApplicationException) {
                formatResponse(((WebApplicationException) instance).getResponse(), text);
            } else if (instance instanceof Response) {
                formatResponse(((Response) instance), text);
            }
            text.append(']');
        }
        return text.toString();
    }

    @Override
    public long timestamp(final RESTEasyTracingEvent 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(RESTEasyTracing.HEADER_RESPONSE_FORMAT, i), messages[i]);
        }
    }
}
