<%#
 Copyright 2013-2023 the original author or authors from the JHipster project.

 This file is part of the JHipster project, see https://www.jhipster.tech/
 for more information.

 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

      https://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 <%= packageName %>.web.rest.errors;

import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation;

import tech.jhipster.config.JHipsterConstants;
import tech.jhipster.web.util.HeaderUtil;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
<%_ if (!databaseTypeNo && !databaseTypeCassandra) { _%>
import org.springframework.dao.ConcurrencyFailureException;
<%_ } _%>
<%_ if (!databaseTypeNo) { _%>
import org.springframework.dao.DataAccessException;
<%_ } _%>
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConversionException;
<%_ if (reactive && databaseTypeSql) { _%>
import org.springframework.stereotype.Component;
<%_ } _%>
import org.springframework.web.ErrorResponseException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
<%_ if (reactive) { _%>
<%_ if (!skipUserManagement) { _%>
import org.springframework.security.core.AuthenticationException;
<%_ } _%>
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.http.MediaType;
import tech.jhipster.web.rest.errors.ExceptionTranslation;
import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
<%_ } _%>
<%_ if (!reactive) { _%>
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
<%_ } _%>
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import tech.jhipster.web.rest.errors.ProblemDetailWithCause;
import tech.jhipster.web.rest.errors.ProblemDetailWithCause.ProblemDetailWithCauseBuilder;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.MethodArgumentNotValidException;

<%_ if (reactive) { _%>
import reactor.core.publisher.Mono;
<%_ } _%>

<%_ if (!reactive) { _%>
import <%= javaVersion === '17' ? 'jakarta' : 'javax'%>.servlet.http.HttpServletRequest;
<%_ } _%>
import java.net.URI;
import java.util.List;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;

/**
 * Controller advice to translate the server side exceptions to client-friendly json structures.
 * The error response follows RFC7807 - Problem Details for HTTP APIs (https://tools.ietf.org/html/rfc7807).
 */
@ControllerAdvice
<%_ if (databaseTypeSql && reactive) { _%>
@Component("jhiExceptionTranslator")
<%_ } _%>
public class ExceptionTranslator extends ResponseEntityExceptionHandler <% if (reactive) { %> implements ExceptionTranslation <% }  %>{

<%_
let returnType;
let requestClass;
let requestEntityRequestClass;
if (reactive) {
    returnType = 'Mono<ResponseEntity<Object>>';
    requestClass = 'ServerWebExchange';
    requestEntityRequestClass = 'ServerWebExchange'
} else {
    returnType = 'ResponseEntity<Object>';
    requestClass = 'NativeWebRequest';
    requestEntityRequestClass = 'WebRequest';
}
_%>
    private static final String FIELD_ERRORS_KEY = "fieldErrors";
    private static final String MESSAGE_KEY = "message";
    private static final String PATH_KEY = "path";
    private static final boolean CASUAL_CHAIN_ENABLED = false;

    @Value("${jhipster.clientApp.name}")
    private String applicationName;

    private final Environment env;

    public ExceptionTranslator(Environment env) {
        this.env = env;
    }

    @ExceptionHandler
    <%_ if (reactive) { _%>@Override<%_ } _%>
    public <%- returnType %> handleAnyException(Throwable ex, <%= requestClass %> request
    ) {
        ProblemDetailWithCause pdCause = wrapAndCustomizeProblem(ex, request);
        return handleExceptionInternal((Exception) ex, pdCause, buildHeaders(ex), HttpStatusCode.valueOf(pdCause.getStatus()), request);
    }

    @Nullable
    @Override
    protected <%- returnType %> handleExceptionInternal(
            Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, <%= requestEntityRequestClass %> request) {
        body = body == null ? wrapAndCustomizeProblem((Throwable) ex, (<%= requestClass %>) request) : body;
    <%_ if (reactive) { _%>
        if (request.getResponse().isCommitted()) {
            return Mono.error(ex);
        }
        return Mono.just(new ResponseEntity<>(body, updateContentType(headers), HttpStatusCode.valueOf(((ProblemDetailWithCause) body).getStatus())));
    <%_ } else { _%>
        return super.handleExceptionInternal(ex, body, headers, statusCode, request);
    <%_ } _%>
    }

    protected ProblemDetailWithCause wrapAndCustomizeProblem(Throwable ex, <%= requestClass %> request) {
        return customizeProblem(getProblemDetailWithCause(ex), ex, request);
    }

    private ProblemDetailWithCause getProblemDetailWithCause(Throwable ex) {
<%_ if (!skipUserManagement) { _%>
        if(ex instanceof <%= packageName %>.service.UsernameAlreadyUsedException ) 
            return (ProblemDetailWithCause) new LoginAlreadyUsedException().getBody();    
        if(ex instanceof <%= packageName %>.service.EmailAlreadyUsedException ) 
            return (ProblemDetailWithCause) new EmailAlreadyUsedException().getBody();
        if(ex instanceof <%= packageName %>.service.InvalidPasswordException )
            return (ProblemDetailWithCause) new InvalidPasswordException().getBody();

        <%_ if (reactive) { _%>
        if (ex instanceof AuthenticationException) {
            // Ensure no information about existing users is revealed via failed authentication attempts
            return ProblemDetailWithCauseBuilder.instance()
                .withStatus(toStatus(ex).value())
                .withTitle("Unauthorized")
                .withDetail("Invalid credentials").build();
        }
        <%_ } _%>
<%_ } _%>
        if(ex instanceof ErrorResponseException exp && exp.getBody() instanceof ProblemDetailWithCause problemDetailWithCause)
            return problemDetailWithCause;
        return ProblemDetailWithCauseBuilder.instance().withStatus(toStatus(ex).value()).build();
    }

    protected ProblemDetailWithCause customizeProblem(ProblemDetailWithCause problem, Throwable err, <%= requestClass %> request) {
        if (problem.getStatus() <= 0) problem.setStatus(toStatus(err));

        if (problem.getType() == null || problem.getType().equals(URI.create("about:blank"))) problem.setType(getMappedType(err));

        // higher precedence to Custom/ResponseStatus types
        String title = extractTitle(err, problem.getStatus());
        String problemTitle = problem.getTitle();
        if (problemTitle == null || !problemTitle.equals(title)) {
        	problem.setTitle(title);
        }

        if (problem.getDetail() == null) {
            // higher precedence to cause
        	problem.setDetail(getCustomizedErrorDetails(err));
        }

        Map<String, Object> problemProperties = problem.getProperties();
        if (problemProperties == null || !problemProperties.containsKey(MESSAGE_KEY))
        	problem.setProperty(MESSAGE_KEY,
                getMappedMessageKey(err) != null
                    ? getMappedMessageKey(err)
                    : "error.http." + problem.getStatus());

        if (problemProperties == null || !problemProperties.containsKey(PATH_KEY))
            problem.setProperty(PATH_KEY, getPathValue(request));

        if((err instanceof <% if (reactive) { %> WebExchangeBindException <% } else { %> MethodArgumentNotValidException <% } %> fieldException) &&
                (problemProperties == null || !problemProperties.containsKey(FIELD_ERRORS_KEY)))
            problem.setProperty(FIELD_ERRORS_KEY, getFieldErrors(fieldException));

        problem.setCause(buildCause(err.getCause(), request).orElse(null));

        return problem;
    }

    private String extractTitle(Throwable err, int statusCode) {
        return getCustomizedTitle(err) != null ? getCustomizedTitle(err) : extractTitleForResponseStatus(err, statusCode);
    }

    private List<FieldErrorVM> getFieldErrors(<% if (reactive) { %>WebExchangeBindException<% } else { %>MethodArgumentNotValidException<% } %> ex) {
        return ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(f ->
                new FieldErrorVM(
                    f.getObjectName().replaceFirst("<%= dtoSuffix %>$", ""),
                    f.getField(),
                    StringUtils.isNotBlank(f.getDefaultMessage()) ? f.getDefaultMessage() : f.getCode()
                )
            )
            .toList();
    }

    private String extractTitleForResponseStatus(Throwable err, int statusCode) {
        ResponseStatus specialStatus = extractResponseStatus(err);
        return specialStatus == null ? HttpStatus.valueOf(statusCode).getReasonPhrase() : specialStatus.reason();
    }

<%_ if (!reactive) { _%>
    private String extractURI(<%= requestClass %> request) {
        HttpServletRequest nativeRequest = request.getNativeRequest(HttpServletRequest.class);
        return nativeRequest != null ? nativeRequest.getRequestURI() : StringUtils.EMPTY;
    }
<%_ } _%>
    private HttpStatus toStatus(final Throwable throwable) {
        // Let the ErrorResponse take this responsibility
        if (throwable instanceof ErrorResponse err) return HttpStatus.valueOf(err.getBody().getStatus());

        return Optional
                .ofNullable(getMappedStatus(throwable))
                .orElse(Optional
                        .ofNullable(resolveResponseStatus(throwable))
                        .map(ResponseStatus::value)
                        .orElse(HttpStatus.INTERNAL_SERVER_ERROR));
    }

    private ResponseStatus extractResponseStatus(final Throwable throwable) {
        return Optional.ofNullable(resolveResponseStatus(throwable))
                .orElse(null);
    }

    private ResponseStatus resolveResponseStatus(final Throwable type) {
        final ResponseStatus candidate = findMergedAnnotation(type.getClass(), ResponseStatus.class);
        return candidate == null && type.getCause() != null ? resolveResponseStatus(type.getCause()) : candidate;
    }

    private URI getMappedType(Throwable err) {
        if(err instanceof MethodArgumentNotValidException)
            return ErrorConstants.CONSTRAINT_VIOLATION_TYPE;
        return ErrorConstants.DEFAULT_TYPE;
    }

    private String getMappedMessageKey(Throwable err) {
        if(err instanceof MethodArgumentNotValidException) {
            return ErrorConstants.ERR_VALIDATION;
    <%_ if (!databaseTypeNo && !databaseTypeCassandra) { _%>
        } else if(err instanceof ConcurrencyFailureException
                || err.getCause() instanceof ConcurrencyFailureException) {
            return ErrorConstants.ERR_CONCURRENCY_FAILURE;
    <%_ } _%>
    <%_ if (reactive) { _%>
        } else if (err instanceof WebExchangeBindException) {
            return ErrorConstants.ERR_VALIDATION;
    <%_ } _%>
        }
        return null;
    }

    private String getCustomizedTitle(Throwable err) {
        if(err instanceof MethodArgumentNotValidException)
            return "Method argument not valid";
        return null;
    }

    private String getCustomizedErrorDetails(Throwable err) {
    	Collection<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
    	if (activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_PRODUCTION)) {
	        if (err instanceof HttpMessageConversionException) return "Unable to convert http message";
    <%_ if (!databaseTypeNo) { _%>
	        if (err instanceof DataAccessException) return "Failure during data access";
    <%_ } _%>
	        if (containsPackageName(err.getMessage())) return "Unexpected runtime exception";
        }
    	return err.getCause() != null ? err.getCause().getMessage() : err.getMessage();
    }

    private HttpStatus getMappedStatus(Throwable err) {
        // Where we disagree with Spring defaults
        if (err instanceof AccessDeniedException) return HttpStatus.FORBIDDEN;
    <%_ if (!databaseTypeNo && !databaseTypeCassandra) { _%>
        if(err instanceof ConcurrencyFailureException) return HttpStatus.CONFLICT;
    <%_ } _%>
        if(err instanceof BadCredentialsException) return HttpStatus.UNAUTHORIZED;
    <%_ if (reactive) { _%>
        if (err instanceof UsernameNotFoundException) return HttpStatus.UNAUTHORIZED;
    <%_ } _%>
        return null;
    }

    private URI getPathValue(<%= requestClass %> request) {
        if(request == null) return URI.create("about:blank");
        return <% if (reactive) { %> request.getRequest().getURI()<% } else { %> URI.create(extractURI(request))<% } %>;
    }

    private HttpHeaders buildHeaders(Throwable err) {
        return err instanceof BadRequestAlertException badRequestAlertException ?
                HeaderUtil.createFailureAlert(applicationName, true, badRequestAlertException.getEntityName(),
                        badRequestAlertException.getErrorKey(), badRequestAlertException.getMessage()) : null;
    }
<%_ if (reactive) { _%>
    private HttpHeaders updateContentType(HttpHeaders headers) {
        if(headers == null) {
            headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_PROBLEM_JSON);
        }
        return headers;
    }
<%_ } _%>

    public Optional<ProblemDetailWithCause> buildCause(final Throwable throwable, <%= requestClass %> request) {
        if(throwable != null && isCasualChainEnabled()) {
            return Optional.of(customizeProblem(getProblemDetailWithCause(throwable), throwable, request));
        }
        return Optional.ofNullable(null);
    }

    private boolean isCasualChainEnabled() {
        // Customize as per the needs
        return CASUAL_CHAIN_ENABLED;
    }

    private boolean containsPackageName(String message) {

        // This list is for sure not complete
        return StringUtils.containsAny(message, "org.", "java.", "net.", "jakarta.", "javax.", "com.", "io.", "de.", "<%= packageName %>");
    }

}
