package dev.lions.unionflow.server.exception;

import dev.lions.unionflow.server.service.SystemLoggingService;
import jakarta.inject.Inject;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.NotAllowedException;
import jakarta.ws.rs.NotAuthorizedException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;

import java.io.PrintWriter;
import java.io.StringWriter;

/**
 * Exception Mapper global pour capturer toutes les exceptions non gérées
 * et les persister dans system_logs.
 *
 * @author UnionFlow Team
 * @version 1.0
 * @since 2026-03-15
 */
@Slf4j
@Provider
public class GlobalExceptionMapper implements ExceptionMapper<Throwable> {

    @Inject
    SystemLoggingService systemLoggingService;

    @Context
    UriInfo uriInfo;

    @Override
    public Response toResponse(Throwable exception) {
        // Déterminer le code HTTP
        int statusCode = determineStatusCode(exception);

        // Les exceptions métier 4xx (404, 401, 403) sont des cas normaux :
        // on les logue en DEBUG/WARN, sans stack trace et sans persister en system_logs.
        if (isExpectedClientError(exception)) {
            log.debug("Expected client error [{}]: {}", statusCode, exception.getMessage());
            return buildErrorResponse(exception, statusCode);
        }

        // Pour toute autre exception (5xx ou inattendue) : log ERROR + persistance
        log.error("Unhandled exception", exception);

        // Récupérer l'endpoint (safe pour les tests unitaires)
        String endpoint = "unknown";
        try {
            if (uriInfo != null) {
                endpoint = uriInfo.getPath();
            }
        } catch (Exception e) {
            // Ignore - pas de contexte REST (ex: test unitaire)
        }

        // Générer le message et le stacktrace
        String message = exception.getMessage() != null ? exception.getMessage() : exception.getClass().getSimpleName();
        String stacktrace = getStackTrace(exception);

        // Persister dans system_logs (ne pas laisser ça crasher le mapper)
        // Note: systemLoggingService est @Transactional → ne peut pas être appelé depuis
        // le thread IO de Vert.x (ex: exceptions levées avant le dispatch vers worker thread)
        try {
            systemLoggingService.logError(
                    determineSource(exception),
                    message,
                    stacktrace,
                    "system",
                    "unknown",
                    "/" + endpoint,
                    statusCode
            );
        } catch (IllegalStateException e) {
            // BlockingOperationNotAllowedException est une IllegalStateException :
            // le mapper est appelé depuis le thread IO, on ne peut pas démarrer une transaction JTA.
            log.debug("Cannot persist error log from IO thread ({}): {}", e.getClass().getSimpleName(), message);
        } catch (Exception e) {
            log.warn("Failed to log error to system_logs", e);
        }

        // Retourner une réponse HTTP appropriée
        return buildErrorResponse(exception, statusCode);
    }

    /**
     * Retourne {@code true} pour les exceptions 4xx qui représentent
     * des cas métier attendus (ressource absente, accès refusé, session expirée).
     * Ces exceptions ne doivent pas être loguées ERROR ni persistées.
     */
    private boolean isExpectedClientError(Throwable exception) {
        return exception instanceof NotFoundException
                || exception instanceof ForbiddenException
                || exception instanceof NotAuthorizedException
                || exception instanceof NotAllowedException
                || exception instanceof IllegalArgumentException
                || exception instanceof IllegalStateException;
    }

    private int determineStatusCode(Throwable exception) {
        if (exception instanceof WebApplicationException webAppException) {
            return webAppException.getResponse().getStatus();
        }

        if (exception instanceof IllegalArgumentException ||
            exception instanceof IllegalStateException) {
            return Response.Status.BAD_REQUEST.getStatusCode();
        }

        if (exception instanceof SecurityException) {
            return Response.Status.FORBIDDEN.getStatusCode();
        }

        return Response.Status.INTERNAL_SERVER_ERROR.getStatusCode();
    }

    private String determineSource(Throwable exception) {
        String className = exception.getClass().getSimpleName();

        if (className.contains("Database") || className.contains("SQL") || className.contains("Persistence")) {
            return "Database";
        }

        if (className.contains("Security") || className.contains("Auth")) {
            return "Auth";
        }

        if (className.contains("Validation")) {
            return "Validation";
        }

        return "API";
    }

    private String getStackTrace(Throwable exception) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        exception.printStackTrace(pw);
        return sw.toString();
    }

    private Response buildErrorResponse(Throwable exception, int statusCode) {
        String message = statusCode >= 500
                ? "Internal server error"
                : (exception.getMessage() != null ? exception.getMessage() : "An error occurred");

        return Response.status(statusCode)
                .entity(java.util.Map.of(
                        "error", message,
                        "status", statusCode,
                        "timestamp", java.time.LocalDateTime.now().toString()
                ))
                .build();
    }
}
