package org.raddatz.familienarchiv.tag;

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

import org.raddatz.familienarchiv.tag.Tag;
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;

public interface TagRepository extends JpaRepository<Tag, UUID> {

    /** Typed projection for document-count aggregation results. */
    interface TagCount {
        UUID getTagId();
        Long getCount();
    }


    // Tag-name resolution (see TagService.findOrCreate). Names that collide case-insensitively across
    // the canonical tree are VALID — a parent and its same-named lowercase child (e.g. "Geburt" /
    // "Geburt/geburt") are distinct nodes with their own source_ref and document attachments. So
    // resolution must be exact-case first, then a non-throwing list for the case-insensitive fallback.
    // Do NOT add a unique(lower(name)) constraint — it would reject these legitimate rows. See #730.
    Optional<Tag> findByName(String name);

    List<Tag> findAllByNameIgnoreCase(String name);

    // Lookup by the canonical tag_path, used for idempotent canonical re-import (Phase 3).
    Optional<Tag> findBySourceRef(String sourceRef);

    List<Tag> findByNameContainingIgnoreCase(String name);

    /**
     * Returns the IDs of all ancestors of the given tag (parent, grandparent, …)
     * via a recursive CTE. Used for cycle detection before assigning a new parent.
     * Includes a depth guard of 50 levels to prevent runaway queries.
     */
    @Query(value = """
            WITH RECURSIVE ancestors AS (
                SELECT parent_id, 0 AS depth
                FROM tag
                WHERE id = :tagId AND parent_id IS NOT NULL
                UNION ALL
                SELECT t.parent_id, a.depth + 1
                FROM tag t
                JOIN ancestors a ON t.id = a.parent_id
                WHERE t.parent_id IS NOT NULL AND a.depth < 50
            )
            SELECT parent_id FROM ancestors
            """, nativeQuery = true)
    List<UUID> findAncestorIds(@Param("tagId") UUID tagId);

    /**
     * Returns the IDs of the tag with the given name AND all of its descendants
     * via a recursive CTE. Used to expand a selected tag to inclusive hierarchy results.
     * Includes a depth guard of 50 levels to prevent runaway queries.
     */
    @Query(value = """
            WITH RECURSIVE descendants AS (
                SELECT id, 0 AS depth FROM tag WHERE LOWER(name) = LOWER(:name)
                UNION ALL
                SELECT t.id, d.depth + 1 FROM tag t
                JOIN descendants d ON t.parent_id = d.id
                WHERE d.depth < 50
            )
            SELECT id FROM descendants
            """, nativeQuery = true)
    List<UUID> findDescendantIdsByName(@Param("name") String name);

    /**
     * Returns the IDs of the tag with the given ID AND all of its descendants
     * via a recursive CTE. Used for merge validation and subtree delete.
     * Includes a depth guard of 50 levels to prevent runaway queries.
     */
    @Query(value = """
            WITH RECURSIVE descendants AS (
                SELECT id, 0 AS depth FROM tag WHERE id = :tagId
                UNION ALL
                SELECT t.id, d.depth + 1 FROM tag t
                JOIN descendants d ON t.parent_id = d.id
                WHERE d.depth < 50
            )
            SELECT id FROM descendants
            """, nativeQuery = true)
    List<UUID> findDescendantIds(@Param("tagId") UUID tagId);

    /**
     * Reassigns document_tags rows from source to target, skipping rows where
     * the target tag is already present (to avoid PK conflicts).
     */
    @Modifying(clearAutomatically = true)
    @Query(value = """
            UPDATE document_tags
            SET tag_id = :targetId
            WHERE tag_id = :sourceId
              AND NOT EXISTS (
                SELECT 1 FROM document_tags d2
                WHERE d2.document_id = document_tags.document_id
                  AND d2.tag_id = :targetId
              )
            """, nativeQuery = true)
    void reassignDocumentTags(@Param("sourceId") UUID sourceId, @Param("targetId") UUID targetId);

    /**
     * Removes all document_tags rows for the given tag.
     */
    @Modifying(clearAutomatically = true)
    @Query(value = "DELETE FROM document_tags WHERE tag_id = :tagId", nativeQuery = true)
    void deleteDocumentTagsByTagId(@Param("tagId") UUID tagId);

    /**
     * Removes all document_tags rows for the given collection of tag IDs.
     * Caller must guard against an empty collection — PostgreSQL rejects IN ().
     */
    @Modifying(clearAutomatically = true)
    @Query(value = "DELETE FROM document_tags WHERE tag_id IN :ids", nativeQuery = true)
    void deleteDocumentTagsByTagIds(@Param("ids") Collection<UUID> ids);

    /**
     * Re-parents all direct children of sourceId to targetId.
     */
    @Modifying(clearAutomatically = true)
    @Query(value = "UPDATE tag SET parent_id = :targetId WHERE parent_id = :sourceId", nativeQuery = true)
    void reparentChildren(@Param("sourceId") UUID sourceId, @Param("targetId") UUID targetId);

    /**
     * Returns (tagId, count) pairs for all tags that appear in document_tags.
     * Used to populate documentCount in the tag tree without N+1 queries.
     */
    @Query(value = "SELECT tag_id AS tagId, COUNT(*) AS count FROM document_tags GROUP BY tag_id", nativeQuery = true)
    List<TagCount> findDocumentCountsPerTag();

    /**
     * Returns (tagId, count) pairs where count is the number of <b>distinct</b> documents tagged
     * with that tag <b>or any of its descendants</b> (full subtree rollup).
     * <p>
     * Builds a tag closure of (ancestor_id, descendant_id) pairs via a recursive CTE — each tag is
     * its own ancestor at depth 0, then descends into children (depth guard of 50 levels prevents a
     * cycle or pathological depth from running away) — joins it to {@code document_tags} on the
     * descendant, and counts distinct documents per ancestor. A document tagged with several tags in
     * the same subtree is therefore counted once. Tags whose entire subtree holds no documents do
     * not appear in the result (they default to 0 in the tree). One aggregate query for all tags.
     */
    @Query(value = """
            WITH RECURSIVE closure AS (
                SELECT id AS ancestor_id, id AS descendant_id, 0 AS depth FROM tag
                UNION ALL
                SELECT c.ancestor_id, t.id AS descendant_id, c.depth + 1
                FROM tag t
                JOIN closure c ON t.parent_id = c.descendant_id
                WHERE c.depth < 50
            )
            SELECT c.ancestor_id AS tagId, COUNT(DISTINCT dt.document_id) AS count
            FROM closure c
            JOIN document_tags dt ON dt.tag_id = c.descendant_id
            GROUP BY c.ancestor_id
            """, nativeQuery = true)
    List<TagCount> findSubtreeDocumentCountsPerTag();
}
