package com.nibado.example.errorhandlers.lib;

import org.slf4j.Logger;
import org.springframework.http.HttpStatus;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

import static org.springframework.http.HttpStatus.*;

public abstract class BaseExceptionHandler {
    private static final ExceptionMapping DEFAULT_ERROR = new ExceptionMapping(
            "SERVER_ERROR",
            "Internal server error",
            INTERNAL_SERVER_ERROR);

    private final Logger log;
    private final Map<Class, ExceptionMapping> exceptionMappings = new HashMap<>();

    public BaseExceptionHandler(final Logger log) {
        this.log = log;

        registerMapping(MissingServletRequestParameterException.class, "MISSING_PARAMETER", "Missing request parameter", BAD_REQUEST);
        registerMapping(MethodArgumentTypeMismatchException.class, "ARGUMENT_TYPE_MISMATCH", "Argument type mismatch", BAD_REQUEST);
        registerMapping(HttpRequestMethodNotSupportedException.class, "METHOD_NOT_SUPPORTED", "HTTP method not supported", METHOD_NOT_ALLOWED);
        registerMapping(ServletRequestBindingException.class, "MISSING_HEADER", "Missing header in request", BAD_REQUEST);
    }

    @ExceptionHandler(Throwable.class)
    @ResponseBody
    public ErrorDTO handleThrowable(final Throwable ex, final HttpServletResponse response) {
        ExceptionMapping mapping = exceptionMappings.getOrDefault(ex.getClass(), DEFAULT_ERROR);

        response.setStatus(mapping.status.value());

        return logAndConstruct(mapping, ex);
    }

    protected void registerMapping(final Class<?> clazz, final String code, final String message, final HttpStatus status) {
        exceptionMappings.put(clazz, new ExceptionMapping(code, message, status));
    }


    protected ErrorDTO construct(ExceptionMapping mapping) {
        return new ErrorDTO(mapping.code, mapping.message);
    }

    protected ErrorDTO logAndConstruct(ExceptionMapping mapping, Throwable t) {
        log.error("{} ({}): {}", mapping.message, mapping.code, t.getMessage(), t);

        return construct(mapping);
    }

    private static class ExceptionMapping {
        private final String message;
        private final String code;
        private final HttpStatus status;

        public ExceptionMapping(final String code, final String message, final HttpStatus status) {
            this.code = code;
            this.message = message;
            this.status = status;
        }
    }
}