/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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.
 */

package io.undertow.servlet.api;

import io.undertow.UndertowLogger;
import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.ExceptionLog;
import org.jboss.logging.BasicLogger;
import org.jboss.logging.Logger;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;

/**
 * An exception handler that
 *
 *
 * @author Stuart Douglas
 */
public class LoggingExceptionHandler implements ExceptionHandler {

    public static final LoggingExceptionHandler DEFAULT = new LoggingExceptionHandler(Collections.<Class<? extends Throwable>, ExceptionDetails>emptyMap());

    private final Map<Class<? extends Throwable>, ExceptionDetails> exceptionDetails;

    public LoggingExceptionHandler(Map<Class<? extends Throwable>, ExceptionDetails> exceptionDetails) {
        this.exceptionDetails = exceptionDetails;
    }

    @Override
    public boolean handleThrowable(HttpServerExchange exchange, ServletRequest request, ServletResponse response, Throwable t) {
        ExceptionDetails details = null;
        if (!exceptionDetails.isEmpty()) {
            Class c = t.getClass();
            while (c != null && c != Object.class) {
                details = exceptionDetails.get(c);
                if (details != null) {
                    break;
                }
                c = c.getSuperclass();
            }
        }

        ExceptionLog log = t.getClass().getAnnotation(ExceptionLog.class);
        if (details != null) {
            Logger.Level level = details.level;
            Logger.Level stackTraceLevel = details.stackTraceLevel;
            String category = details.category;
            handleCustomLog(exchange, t, level, stackTraceLevel, category);
        } else if (log != null) {
            Logger.Level level = log.value();
            Logger.Level stackTraceLevel = log.stackTraceLevel();
            String category = log.category();
            handleCustomLog(exchange, t, level, stackTraceLevel, category);
        } else if (t instanceof IOException) {
            //we log IOExceptions at a lower level
            //because they can be easily caused by malicious remote clients in at attempt to DOS the server by filling the logs
            UndertowLogger.REQUEST_IO_LOGGER.debugf(t, "Exception handling request to %s", exchange.getRequestURI());
        } else {
            UndertowLogger.REQUEST_LOGGER.exceptionHandlingRequest(t, exchange.getRequestURI());
        }
        return false;
    }

    private void handleCustomLog(HttpServerExchange exchange, Throwable t, Logger.Level level, Logger.Level stackTraceLevel, String category) {
        BasicLogger logger = UndertowLogger.REQUEST_LOGGER;
        if (!category.isEmpty()) {
            logger = Logger.getLogger(category);
        }
        boolean stackTrace = true;
        if (stackTraceLevel.ordinal() > level.ordinal()) {
            if (!logger.isEnabled(stackTraceLevel)) {
                stackTrace = false;
            }
        }
        if (stackTrace) {
            logger.logf(level, t, "Exception handling request to %s", exchange.getRequestURI());
        } else {
            logger.logf(level, "Exception handling request to %s: %s", exchange.getRequestURI(), t.getMessage());
        }
    }


    private static class ExceptionDetails {

        final Logger.Level level;

        final Logger.Level stackTraceLevel;

        final String category;

        private ExceptionDetails(Logger.Level level, Logger.Level stackTraceLevel, String category) {
            this.level = level;
            this.stackTraceLevel = stackTraceLevel;
            this.category = category;
        }
    }

    public static Builder builder() {
        return new Builder();
    }

    public static final class Builder {
        private final Map<Class<? extends Throwable>, ExceptionDetails> exceptionDetails = new HashMap<>();

        Builder() {}

        public Builder add(Class<? extends Throwable> exception, String category, Logger.Level level) {
            exceptionDetails.put(exception, new ExceptionDetails(level, Logger.Level.FATAL, category));
            return this;
        }
        public Builder add(Class<? extends Throwable> exception, String category) {
            exceptionDetails.put(exception, new ExceptionDetails(Logger.Level.ERROR, Logger.Level.FATAL, category));
            return this;
        }
        public Builder add(Class<? extends Throwable> exception, String category, Logger.Level level, Logger.Level stackTraceLevel) {
            exceptionDetails.put(exception, new ExceptionDetails(level, stackTraceLevel, category));
            return this;
        }

        public LoggingExceptionHandler build() {
            return new LoggingExceptionHandler(exceptionDetails);
        }
    }
}
