package ai.decompile.common.exception;

import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.time.Instant;
import java.util.Objects;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {
  @ExceptionHandler(NotFoundException.class)
  public ResponseEntity<ErrorResponse> handleNotFound(
      NotFoundException ex, HttpServletRequest request) {
    return buildResponse(HttpStatus.NOT_FOUND, ex, request);
  }

  @ExceptionHandler(ConflictException.class)
  public ResponseEntity<ErrorResponse> handleConflict(
      ConflictException ex, HttpServletRequest request) {
    return buildResponse(HttpStatus.CONFLICT, ex, request);
  }

  @ExceptionHandler(IllegalArgumentException.class)
  public ResponseEntity<ErrorResponse> handleBadRequest(
      IllegalArgumentException ex, HttpServletRequest request) {
    return buildResponse(HttpStatus.BAD_REQUEST, ex, request);
  }

  /**
   * Silently ignores client disconnects to avoid spurious error responses.
   *
   * <p>When an SSE client disconnects mid-stream, the underlying servlet container throws an
   * IOException (broken pipe). Attempting to build a JSON {@link ErrorResponse} fails with {@code
   * HttpMessageNotWritableException} because the response already has {@code Content-Type:
   * text/event-stream} set by the controller — there is no converter for JSON under that media
   * type. Since the client is gone, suppressing the response is the correct behavior.
   */
  @ExceptionHandler(IOException.class)
  public void handleClientDisconnect(IOException ex) {
    if (isBrokenPipe(ex)) {
      return;
    }

    throw new RuntimeException(ex);
  }

  /**
   * Also checks for broken pipe before building an error response, since the root cause may be
   * wrapped in a non-IOException type by the framework (e.g. when the SSE emitter fails during
   * async dispatch).
   */
  @ExceptionHandler(Exception.class)
  public ResponseEntity<ErrorResponse> handleGeneral(Exception ex, HttpServletRequest request) {
    if (isBrokenPipe(ex)) {
      return null;
    }

    return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, ex, request);
  }

  private boolean isBrokenPipe(Exception ex) {
    var message = ex.getMessage();

    if (Objects.isNull(message)) {
      return false;
    }

    return message.contains("Broken pipe")
        || message.contains("Connection reset")
        || message.contains("An existing connection was forcibly closed");
  }

  private ResponseEntity<ErrorResponse> buildResponse(
      HttpStatus status, Exception ex, HttpServletRequest request) {
    var errorResponse =
        new ErrorResponse(
            status.value(),
            status.getReasonPhrase(),
            ex.getMessage(),
            request.getRequestURI(),
            Instant.now());

    return ResponseEntity.status(status).body(errorResponse);
  }
}
