BandListingController.java

package com.wavii.controller;

import com.wavii.dto.band.BandListingResponse;
import com.wavii.dto.band.CreateBandListingRequest;
import com.wavii.model.BandListing;
import com.wavii.model.BandListingReport;
import com.wavii.model.User;
import com.wavii.repository.BandListingReportRepository;
import com.wavii.repository.BandListingRepository;
import com.wavii.service.BandListingService;
import com.wavii.service.OdooService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * Controlador REST para la gestión de anuncios de bandas y músicos.
 * 
 * @author danielrguezh
 */
@RestController
@RequestMapping({ "/api/band-listings", "/api/bands" })
@RequiredArgsConstructor
@Slf4j
public class BandListingController {

    private final BandListingService service;
    private final BandListingRepository bandListingRepository;
    private final BandListingReportRepository bandListingReportRepository;
    private final OdooService odooService;

    @Value("${wavii.app.base-url:http://localhost:8080}")
    private String appBaseUrl;

    @Value("${pdf.storage.path:./uploads/pdfs}")
    private String pdfStoragePath;

    /**
     * Obtiene un listado paginado de anuncios de bandas.
     * 
     * @param genre Género musical (opcional).
     * @param city Ciudad (opcional).
     * @param role Rol del músico (opcional).
     * @param page Número de página.
     * @return Página de resultados.
     */
    @GetMapping
    public ResponseEntity<Page<BandListingResponse>> getListings(
            @RequestParam(required = false) String genre,
            @RequestParam(required = false) String city,
            @RequestParam(required = false) String role,
            @RequestParam(defaultValue = "0") int page
    ) {
        return ResponseEntity.ok(service.getListings(genre, city, role, page));
    }

    /**
     * Obtiene los anuncios creados por el usuario actual.
     * 
     * @param currentUser Usuario autenticado.
     * @return Lista de anuncios del usuario.
     */
    @GetMapping("/my")
    public ResponseEntity<List<BandListingResponse>> getMyListings(
            @AuthenticationPrincipal User currentUser
    ) {
        if (currentUser == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).<List<BandListingResponse>>build();
        }
        try {
            return ResponseEntity.ok(service.getMyListings(currentUser));
        } catch (Exception e) {
            log.error("Error fetching listings for user: {}", e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).<List<BandListingResponse>>build();
        }
    }

    /**
     * Obtiene un anuncio por su ID.
     *
     * @param id ID del anuncio.
     * @return El anuncio encontrado.
     */
    @GetMapping("/{id}")
    public ResponseEntity<BandListingResponse> getListing(@PathVariable UUID id) {
        try {
            return ResponseEntity.ok(service.getById(id));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).<BandListingResponse>build();
        }
    }

    /**
     * Crea un nuevo anuncio de banda.
     *
     * @param request Datos del anuncio.
     * @param currentUser Usuario que crea el anuncio.
     * @return El anuncio creado.
     */
    @PostMapping
    public ResponseEntity<BandListingResponse> create(
            @RequestBody CreateBandListingRequest request,
            @AuthenticationPrincipal User currentUser
    ) {
        if (currentUser == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).<BandListingResponse>build();
        }
        try {
            return ResponseEntity.status(201).body(service.create(request, currentUser));
        } catch (Exception e) {
            log.error("Error creating band listing: {}", e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).<BandListingResponse>build();
        }
    }

    /**
     * Sube una imagen para un anuncio de banda.
     * 
     * @param file Archivo de imagen.
     * @return URL de la imagen subida.
     */
    @PostMapping("/images")
    public ResponseEntity<?> uploadBandImage(@RequestParam("file") MultipartFile file) {
        if (file == null || file.isEmpty()) {
            return ResponseEntity.badRequest().body(Map.of("message", "El archivo no puede estar vacio"));
        }
        String contentType = file.getContentType();
        boolean validImageContentType = contentType != null && contentType.startsWith("image/");
        boolean validImageExtension = hasImageExtension(file.getOriginalFilename());
        if (!validImageContentType && !validImageExtension) {
            return ResponseEntity.badRequest().body(Map.of("message", "Solo se permiten imagenes"));
        }

        try {
            Path uploadPath = uploadRoot().resolve("bands");
            Files.createDirectories(uploadPath);
            String storedName = UUID.randomUUID() + extensionOf(file.getOriginalFilename());
            log.info("Subiendo imagen de banda: contentType={}, size={} bytes", contentType, file.getSize());
            Files.copy(file.getInputStream(), uploadPath.resolve(storedName), StandardCopyOption.REPLACE_EXISTING);
            return ResponseEntity.ok(Map.of(
                    "url", appBaseUrl + "/uploads/bands/" + storedName,
                    "fileName", storedName
            ));
        } catch (IOException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(Map.of("message", "No se pudo guardar la imagen"));
        }
    }

    @PostMapping("/{id}/report")
    public ResponseEntity<?> reportBandListing(
            @PathVariable UUID id,
            @AuthenticationPrincipal User currentUser,
            @RequestBody ReportBandListingRequest request
    ) {
        if (currentUser == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "Sesion no valida"));
        }
        if (request.reason() == null || request.reason().isBlank()) {
            return ResponseEntity.badRequest().body(Map.of("message", "Debes indicar un motivo"));
        }

        BandListing listing = bandListingRepository.findById(id).orElse(null);
        if (listing == null || listing.isRemovedByModeration()) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", "Anuncio no encontrado"));
        }

        BandListingReport report = BandListingReport.builder()
                .listing(listing)
                .reporter(currentUser)
                .reason(request.reason().trim())
                .details(request.details() != null ? request.details().trim() : null)
                .build();
        bandListingReportRepository.save(report);
        Map<String, Object> snapshot = new LinkedHashMap<>();
        snapshot.put("listingTitle", listing.getTitle());
        snapshot.put("listingDescription", listing.getDescription());
        snapshot.put("listingType", listing.getType().name());
        snapshot.put("listingGenre", listing.getGenre().name());
        snapshot.put("listingCity", listing.getCity());
        snapshot.put("listingRoles", listing.getRoles().stream().map(Enum::name).toList());
        snapshot.put("creatorId", listing.getCreator().getId().toString());
        snapshot.put("creatorName", listing.getCreator().getName());
        snapshot.put("creatorEmail", listing.getCreator().getEmail());
        snapshot.put("reporterId", currentUser.getId().toString());
        snapshot.put("reportId", report.getId().toString());
        odooService.createModerationReport(
                "band_listing",
                currentUser.getName(),
                currentUser.getEmail(),
                listing.getTitle(),
                "band_listing:" + listing.getId(),
                report.getReason(),
                report.getDetails(),
                snapshot
        );
        return ResponseEntity.ok(Map.of("message", "Reporte enviado"));
    }

    /**
     * Elimina un anuncio de banda.
     * 
     * @param id ID del anuncio a eliminar.
     * @param currentUser Usuario autenticado.
     * @return 204 No Content si se eliminó correctamente.
     */
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(
            @PathVariable UUID id,
            @AuthenticationPrincipal User currentUser
    ) {
        if (currentUser == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        try {
            service.delete(id, currentUser);
            return ResponseEntity.noContent().build();
        } catch (Exception e) {
            log.error("Error deleting band listing {}: {}", id, e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    @ExceptionHandler(MultipartException.class)
    public ResponseEntity<Map<String, String>> handleMultipartException(MultipartException ex) {
        log.warn("No se pudo procesar la subida de imagen de banda: {}", ex.getMessage());
        return ResponseEntity.badRequest().body(Map.of(
                "message", "No se pudo leer la imagen. Prueba con otra foto o vuelve a intentarlo."
        ));
    }

    /**
     * Obtiene la extensión de un nombre de archivo.
     * 
     * @param originalFilename Nombre original.
     * @return Extensión (por defecto .jpg).
     */
    private String extensionOf(String originalFilename) {
        if (originalFilename == null || !originalFilename.contains(".")) {
            return ".jpg";
        }
        String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
        return extension.isBlank() ? ".jpg" : extension;
    }

    private boolean hasImageExtension(String originalFilename) {
        if (originalFilename == null || !originalFilename.contains(".")) {
            return false;
        }
        String extension = originalFilename.substring(originalFilename.lastIndexOf('.') + 1).toLowerCase();
        return extension.equals("jpg")
                || extension.equals("jpeg")
                || extension.equals("png")
                || extension.equals("webp")
                || extension.equals("gif")
                || extension.equals("heic");
    }

    /**
     * Obtiene la ruta raíz para las subidas.
     * 
     * @return Path de la raíz de subidas.
     */
    private Path uploadRoot() {
        Path root = Paths.get(pdfStoragePath).getParent();
        return root != null ? root : Paths.get("uploads");
    }

    record ReportBandListingRequest(String reason, String details) {
    }
}