package com.iqser.red.persistence.service.v1.external.api.impl.controller;

import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.iqser.red.commons.spring.ErrorMessage;
import com.iqser.red.service.persistence.management.v1.processor.exception.BadRequestException;
import com.iqser.red.service.persistence.management.v1.processor.exception.ConflictException;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotAllowedException;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotFoundException;
import com.knecon.fforesight.tenantcommons.TenantContext;
import com.mchange.rmi.NotAuthorizedException;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobDataMap;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.support.MissingServletRequestPartException;

import java.time.OffsetDateTime;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@RequiredArgsConstructor
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ExternalControllerAdvice {

    private final Scheduler scheduler;


    @Hidden
    @ResponseBody
    @ResponseStatus(value = HttpStatus.NOT_FOUND)
    @ExceptionHandler(value = NotFoundException.class)
    public ErrorMessage handleContentNotFoundException(NotFoundException e) {

        return new ErrorMessage(OffsetDateTime.now(), e.getMessage());
    }


    /* error handling */

    @Hidden
    @ResponseBody
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = BadRequestException.class)
    public ErrorMessage handleBadRequestException(BadRequestException e) {

        return new ErrorMessage(OffsetDateTime.now(), e.getMessage());
    }

    @Hidden
    @ResponseBody
    @ResponseStatus(value = HttpStatus.CONFLICT)
    @ExceptionHandler(value = {ConflictException.class})
    protected ErrorMessage handleConflictException(ConflictException e) {

        return new ErrorMessage(OffsetDateTime.now(), e.getMessage());
    }

    @ResponseBody
    @ResponseStatus(value = HttpStatus.FORBIDDEN)
    @ExceptionHandler({AccessDeniedException.class})
    public ErrorMessage handleAccessDeniedException(AccessDeniedException e) {

        return new ErrorMessage(OffsetDateTime.now(), e.getMessage());
    }

    @ResponseBody
    @ResponseStatus(value = HttpStatus.UNAUTHORIZED)
    @ExceptionHandler({NotAuthorizedException.class})
    public ErrorMessage handleNotAuthorizedException(NotAuthorizedException e) {
        return new ErrorMessage(OffsetDateTime.now(), e.getMessage());
    }


    @ResponseBody
    @ResponseStatus(value = HttpStatus.FORBIDDEN)
    @ExceptionHandler({NotAllowedException.class})
    public ErrorMessage handleNotAllowedException(NotAllowedException e) {

        return new ErrorMessage(OffsetDateTime.now(), e.getMessage());
    }

    @Hidden
    @ResponseBody
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler({org.springframework.security.acls.model.NotFoundException.class})
    public ErrorMessage handleACLNotFound(org.springframework.security.acls.model.NotFoundException e) {

        // in case this error occurs on a rest request / force trigger the sync job
        try {
            scheduler.triggerJob(new JobKey("SyncUserPermissionsJob"), new JobDataMap(Map.of("tenantId", TenantContext.getTenantId())));
        } catch (SchedulerException ex) {
            log.debug("Failed to force trigger SyncUserPermissionsJob", ex);
        }

        return new ErrorMessage(OffsetDateTime.now(), e.getMessage());
    }

    @Hidden
    @ResponseBody
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public ErrorMessage handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {

        var errorList = e.getFieldErrors();
        String errorListAsString = errorList.stream().map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage()).collect(Collectors.joining(", "));
        return new ErrorMessage(OffsetDateTime.now(), String.format("You have empty/wrong formatted parameters: %s", errorListAsString));
    }

    @Hidden
    @ResponseBody
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler({MissingServletRequestPartException.class})
    public ErrorMessage handleMissingServletRequestPartException(MissingServletRequestPartException e) {

        return new ErrorMessage(OffsetDateTime.now(), e.getMessage());
    }

    @Hidden
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler({HttpMessageNotReadableException.class})
    public ErrorMessage handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {

        var cause = e.getCause();
        if (cause instanceof InvalidFormatException) {
            InvalidFormatException invalidFormatException = (InvalidFormatException) cause;

            Class<?> targetType = invalidFormatException.getTargetType();
            if (targetType != null && targetType.isEnum()) {
                return new ErrorMessage(OffsetDateTime.now(), String.format("Unsupported value for %s", targetType.getSimpleName()));
            }

            return new ErrorMessage(OffsetDateTime.now(), cause.getMessage());
        }

        return new ErrorMessage(OffsetDateTime.now(), e.getMessage());
    }


    @Order(10000)
    public static class BinderControllerAdvice {

        @InitBinder
        public void setAllowedFields(WebDataBinder dataBinder) {
            // This code protects Spring Core from a "Remote Code Execution" attack (dubbed "Spring4Shell").
            // By applying this mitigation, you prevent the "Class Loader Manipulation" attack vector from firing.
            // For more details, see this post: https://www.lunasec.io/docs/blog/spring-rce-vulnerabilities/
            String[] denylist = new String[]{"class.*", "Class.*", "*.class.*", "*.Class.*"};
            dataBinder.setDisallowedFields(denylist);
        }

    }

}
