ForumService.java

package com.wavii.service;

import com.wavii.dto.forum.CreateForumRequest;
import com.wavii.dto.forum.CreatePostRequest;
import com.wavii.dto.forum.ForumMemberResponse;
import com.wavii.dto.forum.ForumResponse;
import com.wavii.dto.forum.ForumSummaryResponse;
import com.wavii.dto.forum.PostResponse;
import com.wavii.dto.forum.UpdateForumRequest;
import com.wavii.model.Forum;
import com.wavii.model.ForumLike;
import com.wavii.model.ForumMembership;
import com.wavii.model.ForumPost;
import com.wavii.model.User;
import com.wavii.model.enums.ForumCategory;
import com.wavii.model.enums.ForumMembershipRole;
import com.wavii.model.enums.ForumSortOption;
import com.wavii.repository.ForumLikeRepository;
import com.wavii.repository.ForumMembershipRepository;
import com.wavii.repository.ForumPostRepository;
import com.wavii.repository.ForumRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import jakarta.persistence.criteria.Predicate;
import java.util.stream.Collectors;

/**
 * Servicio para la gestión de foros y comunidades en Wavii.
 * Proporciona métodos para crear foros, gestionar membresías, publicaciones e interacciones.
 * 
 * @author danielrguezh
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class ForumService {

    private final ForumRepository forumRepository;
    private final ForumMembershipRepository membershipRepository;
    private final ForumPostRepository postRepository;
    private final ForumLikeRepository likeRepository;

    /**
     * Obtiene la lista de foros filtrados por búsqueda, ciudad y categoría.
     * 
     * @param search Texto a buscar en el nombre.
     * @param city Ciudad para filtrar.
     * @param category Categoría del foro.
     * @param sort Criterio de ordenación.
     * @param currentUser Usuario actual para marcar estados de pertenencia y likes.
     * @return Lista de resúmenes de foros.
     */
    @Transactional(readOnly = true)
    public List<ForumSummaryResponse> getForums(String search, String city, String category, String sort, User currentUser) {
        String normalizedSearch = search != null && !search.isBlank() ? search.trim().toLowerCase() : null;
        String normalizedCity = city != null && !city.isBlank() ? city.trim().toLowerCase() : null;
        ForumCategory categoryFilter = parseEnum(ForumCategory.class, category);
        ForumSortOption sortOption = parseEnum(ForumSortOption.class, sort);
        Sort sortOrder = switch (sortOption != null ? sortOption : ForumSortOption.MOST_LIKED) {
            case NEWEST -> Sort.by(Sort.Direction.DESC, "createdAt");
            case OLDEST -> Sort.by(Sort.Direction.ASC, "createdAt");
            case LEAST_LIKED -> Sort.by(Sort.Direction.ASC, "likeCount").and(Sort.by(Sort.Direction.DESC, "createdAt"));
            case MOST_LIKED -> Sort.by(Sort.Direction.DESC, "likeCount").and(Sort.by(Sort.Direction.DESC, "createdAt"));
        };

        Specification<Forum> spec = (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();
            if (normalizedSearch != null) {
                predicates.add(cb.like(cb.lower(root.get("name")), "%" + normalizedSearch + "%"));
            }
            if (normalizedCity != null) {
                predicates.add(cb.like(cb.lower(root.get("city")), "%" + normalizedCity + "%"));
            }
            if (categoryFilter != null) {
                predicates.add(cb.equal(root.get("category"), categoryFilter));
            }
            return cb.and(predicates.toArray(new Predicate[0]));
        };

        List<Forum> forums = forumRepository
                .findAll(spec, PageRequest.of(0, 50, sortOrder))
                .getContent();

        Set<UUID> joinedIds = Collections.emptySet();
        Set<UUID> likedIds = Collections.emptySet();
        if (currentUser != null && !forums.isEmpty()) {
            List<UUID> forumIds = forums.stream().map(Forum::getId).collect(Collectors.toList());
            joinedIds = new HashSet<>(membershipRepository.findJoinedForumIds(currentUser, forumIds));
            likedIds = new HashSet<>(likeRepository.findLikedForumIds(currentUser, forumIds));
        }

        final Set<UUID> finalJoinedIds = joinedIds;
        final Set<UUID> finalLikedIds = likedIds;
        return forums.stream()
                .map((forum) -> toSummary(forum, finalJoinedIds.contains(forum.getId()), finalLikedIds.contains(forum.getId())))
                .toList();
    }

    /**
     * Obtiene los detalles de un foro específico por su ID.
     * 
     * @param id ID del foro.
     * @param currentUser Usuario actual.
     * @return Respuesta con los detalles del foro.
     */
    @Transactional(readOnly = true)
    public ForumResponse getForumById(UUID id, User currentUser) {
        Forum forum = forumRepository.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Foro no encontrado"));
        boolean joined = currentUser != null && membershipRepository.existsByForumAndUser(forum, currentUser);
        boolean liked = currentUser != null && likeRepository.existsByForumAndUser(forum, currentUser);
        return toResponse(forum, joined, liked, currentUser);
    }

    /**
     * Crea un nuevo foro en la plataforma.
     * 
     * @param request Datos de creación.
     * @param creator Usuario creador.
     * @return Respuesta con el foro creado.
     */
    @Transactional
    public ForumResponse createForum(CreateForumRequest request, User creator) {
        if (request.name() == null || request.name().isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "El nombre es obligatorio");
        }
        if (request.category() == null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "La categoria es obligatoria");
        }

        Forum forum = Forum.builder()
                .name(request.name().trim())
                .description(request.description() != null ? request.description().trim() : null)
                .category(request.category())
                .coverImageUrl(request.coverImageUrl())
                .city(request.city() != null && !request.city().isBlank() ? request.city().trim() : null)
                .creator(creator)
                .memberCount(1)
                .build();
        forum = forumRepository.save(forum);

        membershipRepository.save(ForumMembership.builder()
                .forum(forum)
                .user(creator)
                .role(ForumMembershipRole.OWNER)
                .build());

        return toResponse(forum, true, false, creator);
    }

    @Transactional
    public ForumResponse updateForum(UUID forumId, UpdateForumRequest request, User currentUser) {
        Forum forum = forumRepository.findById(forumId)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Foro no encontrado"));

        ForumMembership membership = membershipRepository.findByForumAndUser(forum, currentUser)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN, "No tienes acceso a este foro"));

        if (membership.getRole() != ForumMembershipRole.OWNER && membership.getRole() != ForumMembershipRole.ADMIN) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Solo los administradores pueden editar el grupo");
        }

        if (request.description() != null) {
            forum.setDescription(request.description().trim().isEmpty() ? null : request.description().trim());
        }
        if (request.coverImageUrl() != null) {
            forum.setCoverImageUrl(request.coverImageUrl());
        }
        if (request.category() != null) {
            forum.setCategory(request.category());
        }

        forum = forumRepository.save(forum);
        boolean liked = likeRepository.existsByForumAndUser(forum, currentUser);
        return toResponse(forum, true, liked, currentUser);
    }

    /**
     * Une al usuario actual a un foro.
     *
     * @param forumId ID del foro.
     * @param user Usuario actual.
     */
    @Transactional
    public void joinForum(UUID forumId, User user) {
        Forum forum = forumRepository.findById(forumId)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Foro no encontrado"));

        if (membershipRepository.existsByForumAndUser(forum, user)) {
            throw new ResponseStatusException(HttpStatus.CONFLICT, "Ya eres miembro de este foro");
        }

        membershipRepository.save(ForumMembership.builder()
                .forum(forum)
                .user(user)
                .build());
        forumRepository.incrementMemberCount(forumId);
    }

    /**
     * Hace que el usuario abandone un foro.
     * 
     * @param forumId ID del foro.
     * @param user Usuario actual.
     */
    @Transactional
    public void leaveForum(UUID forumId, User user) {
        Forum forum = forumRepository.findById(forumId)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Foro no encontrado"));

        ForumMembership membership = membershipRepository.findByForumAndUser(forum, user)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No eres miembro de este foro"));

        membershipRepository.delete(membership);
        reconcileAfterMemberRemoval(forum);
    }

    /**
     * Obtiene la lista de foros a los que pertenece el usuario.
     * 
     * @param user Usuario actual.
     * @return Lista de resúmenes de sus foros.
     */
    @Transactional(readOnly = true)
    public List<ForumSummaryResponse> getMyForums(User user) {
        return membershipRepository.findByUserWithForum(user).stream()
                .map((membership) -> toSummary(membership.getForum(), true,
                        likeRepository.existsByForumAndUser(membership.getForum(), user)))
                .toList();
    }

    /**
     * Lista los miembros de un foro.
     * 
     * @param forumId ID del foro.
     * @param requester Usuario que realiza la consulta (debe ser miembro).
     * @return Lista de miembros con sus roles.
     */
    @Transactional(readOnly = true)
    public List<ForumMemberResponse> getMembers(UUID forumId, User requester) {
        Forum forum = findForum(forumId);
        ensureMember(forum, requester);
        return getMemberResponses(forum);
    }

    /**
     * Registra un "me gusta" en un foro.
     * 
     * @param forumId ID del foro.
     * @param user Usuario actual.
     * @return Respuesta actualizada del foro.
     */
    @Transactional
    public ForumResponse likeForum(UUID forumId, User user) {
        Forum forum = findForum(forumId);
        if (!likeRepository.existsByForumAndUser(forum, user)) {
            likeRepository.save(ForumLike.builder().forum(forum).user(user).build());
            forumRepository.incrementLikeCount(forumId);
            forum.setLikeCount(forum.getLikeCount() + 1);
        }
        boolean joined = membershipRepository.existsByForumAndUser(forum, user);
        return toResponse(forum, joined, true, user);
    }

    /**
     * Retira un "me gusta" de un foro.
     * 
     * @param forumId ID del foro.
     * @param user Usuario actual.
     * @return Respuesta actualizada del foro.
     */
    @Transactional
    public ForumResponse unlikeForum(UUID forumId, User user) {
        Forum forum = findForum(forumId);
        likeRepository.findByForumAndUser(forum, user).ifPresent(like -> {
            likeRepository.delete(like);
            forumRepository.decrementLikeCount(forumId);
            forum.setLikeCount(Math.max(0, forum.getLikeCount() - 1));
        });
        boolean joined = membershipRepository.existsByForumAndUser(forum, user);
        return toResponse(forum, joined, false, user);
    }

    /**
     * Actualiza el rol de un miembro del foro (solo permitido para el OWNER).
     * 
     * @param forumId ID del foro.
     * @param userId ID del miembro a actualizar.
     * @param role Nuevo rol.
     * @param requester Usuario que realiza la acción.
     * @return Respuesta actualizada del foro.
     */
    @Transactional
    public ForumResponse updateMemberRole(UUID forumId, UUID userId, ForumMembershipRole role, User requester) {
        Forum forum = findForum(forumId);
        ForumMembership requesterMembership = ensureMember(forum, requester);
        if (requesterMembership.getRole() != ForumMembershipRole.OWNER) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Solo el creador puede cambiar administradores");
        }
        if (role == null || role == ForumMembershipRole.OWNER) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Rol no valido");
        }
        ForumMembership target = membershipRepository.findByForumOrderByJoinedAtAsc(forum).stream()
                .filter(m -> m.getUser().getId().equals(userId))
                .findFirst()
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Miembro no encontrado"));
        if (target.getRole() == ForumMembershipRole.OWNER) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No puedes cambiar el rol del creador");
        }
        target.setRole(role);
        membershipRepository.save(target);
        return toResponse(forum, true, likeRepository.existsByForumAndUser(forum, requester), requester);
    }

    /**
     * Expulsa a un miembro del foro (solo permitido para ADMIN u OWNER).
     * 
     * @param forumId ID del foro.
     * @param userId ID del miembro a expulsar.
     * @param requester Usuario que realiza la acción.
     */
    @Transactional
    public void removeMember(UUID forumId, UUID userId, User requester) {
        Forum forum = findForum(forumId);
        ForumMembership requesterMembership = ensureMember(forum, requester);
        if (requesterMembership.getRole() == ForumMembershipRole.MEMBER) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Solo admins pueden expulsar miembros");
        }
        ForumMembership target = membershipRepository.findByForumOrderByJoinedAtAsc(forum).stream()
                .filter(m -> m.getUser().getId().equals(userId))
                .findFirst()
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Miembro no encontrado"));
        if (target.getRole() == ForumMembershipRole.OWNER) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No puedes expulsar al creador");
        }
        membershipRepository.delete(target);
        reconcileAfterMemberRemoval(forum);
    }

    /**
     * Obtiene los mensajes (posts) de un foro en formato paginado.
     * 
     * @param forumId ID del foro.
     * @param page Número de página.
     * @param currentUser Usuario actual.
     * @return Página de mensajes.
     */
    @Transactional(readOnly = true)
    public Page<PostResponse> getPosts(UUID forumId, int page, User currentUser) {
        Forum forum = forumRepository.findById(forumId)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Foro no encontrado"));

        if (!membershipRepository.existsByForumAndUser(forum, currentUser)) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Debes unirte al foro para ver los mensajes");
        }

        return postRepository.findByForumOrderByCreatedAtDesc(forum, PageRequest.of(page, 20))
                .map(this::toPostResponse);
    }

    @Transactional
    public PostResponse createPost(UUID forumId, CreatePostRequest request, User author) {
        if (request.content() == null || request.content().isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "El mensaje no puede estar vacio");
        }

        Forum forum = forumRepository.findById(forumId)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Foro no encontrado"));

        if (!membershipRepository.existsByForumAndUser(forum, author)) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Debes unirte al foro para escribir mensajes");
        }

        ForumPost post = ForumPost.builder()
                .forum(forum)
                .author(author)
                .content(request.content().trim())
                .build();

        return toPostResponse(postRepository.save(post));
    }

    private ForumSummaryResponse toSummary(Forum forum, boolean joined, boolean likedByMe) {
        return new ForumSummaryResponse(
                forum.getId().toString(),
                forum.getName(),
                forum.getDescription(),
                forum.getCategory().name(),
                forum.getMemberCount(),
                joined,
                forum.getCoverImageUrl(),
                forum.getCreator().getName(),
                forum.getCity(),
                forum.getLikeCount(),
                likedByMe
        );
    }

    private ForumResponse toResponse(Forum forum, boolean joined, boolean likedByMe, User currentUser) {
        String role = currentUser == null ? null : membershipRepository.findByForumAndUser(forum, currentUser)
                .map(membership -> membership.getRole().name())
                .orElse(null);
        return new ForumResponse(
                forum.getId().toString(),
                forum.getName(),
                forum.getDescription(),
                forum.getCategory().name(),
                forum.getMemberCount(),
                joined,
                forum.getCoverImageUrl(),
                forum.getCreator().getId().toString(),
                forum.getCreator().getName(),
                forum.getCity(),
                forum.getCreatedAt().toString(),
                forum.getLikeCount(),
                likedByMe,
                role,
                getMemberResponses(forum)
        );
    }

    private PostResponse toPostResponse(ForumPost post) {
        return new PostResponse(
                post.getId().toString(),
                post.getContent(),
                post.getAuthor().getId().toString(),
                post.getAuthor().getName(),
                post.getAuthor().getAvatarUrl(),
                post.getCreatedAt().toString()
        );
    }

    private Forum findForum(UUID forumId) {
        return forumRepository.findById(forumId)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Foro no encontrado"));
    }

    private ForumMembership ensureMember(Forum forum, User user) {
        return membershipRepository.findByForumAndUser(forum, user)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN, "Debes unirte al foro"));
    }

    private List<ForumMemberResponse> getMemberResponses(Forum forum) {
        return membershipRepository.findByForumOrderByJoinedAtAsc(forum).stream()
                .map((membership) -> new ForumMemberResponse(
                        membership.getUser().getId().toString(),
                        membership.getUser().getName(),
                        membership.getUser().getAvatarUrl(),
                        membership.getRole().name(),
                        membership.getJoinedAt().toString()
                ))
                .toList();
    }

    private void reconcileAfterMemberRemoval(Forum forum) {
        List<ForumMembership> remaining = membershipRepository.findByForumOrderByJoinedAtAsc(forum);
        if (remaining.isEmpty()) {
            deleteForum(forum);
            return;
        }

        forum.setMemberCount(remaining.size());
        boolean hasOwner = remaining.stream().anyMatch(membership -> membership.getRole() == ForumMembershipRole.OWNER);
        if (!hasOwner) {
            ForumMembership nextOwner = remaining.stream()
                    .filter(membership -> membership.getRole() == ForumMembershipRole.ADMIN)
                    .findFirst()
                    .orElse(remaining.get(0));
            nextOwner.setRole(ForumMembershipRole.OWNER);
            forum.setCreator(nextOwner.getUser());
            membershipRepository.save(nextOwner);
        }
        forumRepository.save(forum);
    }

    private void deleteForum(Forum forum) {
        postRepository.deleteByForum(forum);
        likeRepository.deleteByForum(forum);
        membershipRepository.deleteByForum(forum);
        forumRepository.delete(forum);
    }

    private <E extends Enum<E>> E parseEnum(Class<E> cls, String value) {
        if (value == null || value.isBlank()) return null;
        try {
            return Enum.valueOf(cls, value.trim().toUpperCase());
        } catch (IllegalArgumentException e) {
            return null;
        }
    }
}