package org.raddatz.familienarchiv.geschichte.journeyitem;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

@Repository
public interface JourneyItemRepository extends JpaRepository<JourneyItem, UUID> {

    /** Returns items ordered by position ASC for the read-model assembly path. */
    List<JourneyItem> findByGeschichteIdOrderByPosition(UUID geschichteId);

    /** IDOR-safe lookup: returns empty when itemId exists but belongs to a different journey. */
    Optional<JourneyItem> findByIdAndGeschichteId(UUID id, UUID geschichteId);

    /** Returns only the IDs — used for set-equality check in reorder. */
    @Query("SELECT i.id FROM JourneyItem i WHERE i.geschichte.id = :geschichteId")
    Set<UUID> findIdsByGeschichteId(@Param("geschichteId") UUID geschichteId);

    /** MAX position for computing the next append position; returns empty when journey has no items. */
    @Query("SELECT MAX(i.position) FROM JourneyItem i WHERE i.geschichte.id = :geschichteId")
    Optional<Integer> findMaxPositionByGeschichteId(@Param("geschichteId") UUID geschichteId);

    /** COUNT for the 100-item cap check — COUNT(*)-based, never MAX(position)-derived. */
    long countByGeschichteId(UUID geschichteId);

    /**
     * Dedup guard: true when the document is already linked to this journey.
     * Explicit JPQL, not a derived query: the transient {@code getDocumentId()}
     * getter on JourneyItem makes Spring Data resolve the derived path as a
     * direct {@code documentId} attribute, which Hibernate cannot map.
     */
    @Query("""
            SELECT COUNT(i) > 0 FROM JourneyItem i
            WHERE i.geschichte.id = :geschichteId AND i.document.id = :documentId
            """)
    boolean existsByGeschichteIdAndDocumentId(
            @Param("geschichteId") UUID geschichteId, @Param("documentId") UUID documentId);

    /**
     * Deletes note-less items (note IS NULL or note = '') linked to the given document.
     * Used by JourneyItemDocumentDeleteListener before the document row is removed, so
     * the FK ON DELETE SET NULL never fires on rows that would violate chk_journey_item_not_empty.
     * Explicit JPQL — same trap as existsByGeschichteIdAndDocumentId: the transient
     * getDocumentId() getter makes Spring Data unable to resolve a derived query path.
     * clearAutomatically = true invalidates the L1 cache so AC-2's "note-carrying survives"
     * assertion never reads a stale entity. flushAutomatically = true makes the
     * flush-before-delete contract explicit rather than relying on Hibernate AUTO flush mode.
     */
    @Modifying(clearAutomatically = true, flushAutomatically = true)
    @Query("DELETE FROM JourneyItem i WHERE i.document.id = :documentId AND (i.note IS NULL OR i.note = '')")
    int deleteNoteLessByDocumentId(@Param("documentId") UUID documentId);

    /**
     * Loads journey items with their linked Document in a single JOIN FETCH query,
     * eliminating the N+1 SELECT that would occur when accessing item.getDocument()
     * lazily for each item. Items without a document (note-only) are included via
     * LEFT JOIN. Ordered by position ASC.
     */
    @Query("SELECT ji FROM JourneyItem ji LEFT JOIN FETCH ji.document WHERE ji.geschichte.id = :geschichteId ORDER BY ji.position ASC")
    List<JourneyItem> findByGeschichteIdWithDocument(@Param("geschichteId") UUID geschichteId);
}
