package org.raddatz.familienarchiv.document;

import org.raddatz.familienarchiv.document.transcription.TranscriptionQueueProjection;
import org.raddatz.familienarchiv.document.transcription.TranscriptionWeeklyStatsProjection;
import org.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.document.DocumentStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

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

@Repository
public interface DocumentRepository extends JpaRepository<Document, UUID>, JpaSpecificationExecutor<Document> {

    @EntityGraph("Document.full")
    Optional<Document> findById(UUID id);

    @EntityGraph("Document.list")
    Page<Document> findAll(Specification<Document> spec, Pageable pageable);

    @EntityGraph("Document.list")
    List<Document> findAll(Specification<Document> spec);

    @EntityGraph("Document.list")
    Page<Document> findAll(Pageable pageable);

    // Loader for the relevance fast path: list-item enrichment reads tags after the
    // repository call returns, so the fetch shape must match the spec-based findAll
    // overloads above. Plain findAllById carries no entity graph and must not feed
    // enrichItems — see DocumentService.relevanceSortedPageFromSql.
    @EntityGraph("Document.list")
    List<Document> findByIdIn(Collection<UUID> ids);

    // Findet ein Dokument anhand des ursprünglichen Dateinamens
    // Wichtig für den Abgleich beim Excel-Import & Datei-Upload
    Optional<Document> findByOriginalFilename(String originalFilename);

    // Wie oben, gibt aber nur das erste Ergebnis zurück — sicher wenn doppelte Dateinamen existieren
    Optional<Document> findFirstByOriginalFilename(String originalFilename);

    // Callers access only status/id scalar fields — no graph needed.
    List<Document> findByStatus(DocumentStatus status);

    // Prüft effizient, ob ein Dateiname schon existiert (gibt true/false zurück)
    boolean existsByOriginalFilename(String originalFilename);

    // Bulk-fetch for global timeline path — single query with sender+receivers eager-loaded.
    @EntityGraph("Document.list")
    @Query("SELECT d FROM Document d")
    List<Document> findAllForTimeline();

    // lazy – @BatchSize(50) fallback active; see ADR-022
    @EntityGraph("Document.full")
    List<Document> findBySenderId(UUID senderId);

    // lazy – @BatchSize(50) fallback active; see ADR-022
    @EntityGraph("Document.full")
    List<Document> findByReceiversId(UUID receiverId);


    // Callers access only doc.getTags() to mutate the set — receivers/sender not touched; no graph needed.
    List<Document> findByTags_Id(UUID tagId);

    @Query("SELECT d FROM Document d WHERE d.id NOT IN (SELECT DISTINCT dv.documentId FROM DocumentVersion dv)")
    List<Document> findDocumentsWithoutVersions();

    List<Document> findByFileHashIsNullAndFilePathIsNotNull();

    List<Document> findByFilePathIsNotNullAndThumbnailKeyIsNull();

    @Query("SELECT d.id, d.title FROM Document d WHERE d.id IN :ids")
    List<Object[]> findIdAndTitleByIdIn(@Param("ids") Collection<UUID> ids);

    long countByMetadataCompleteFalse();

    // No production callers — only used if a future export path iterates the full list; no graph needed.
    List<Document> findByMetadataCompleteFalse(Sort sort);

    // Callers map to IncompleteDocumentDTO using only scalar fields (id, title, createdAt) — no graph needed.
    Page<Document> findByMetadataCompleteFalse(Pageable pageable);

    Optional<Document> findFirstByMetadataCompleteFalseAndIdNot(UUID id, Sort sort);

    @Query(nativeQuery = true, value = """
            SELECT d.id FROM documents d
            CROSS JOIN LATERAL (
                SELECT CASE WHEN websearch_to_tsquery('german', :query)::text <> ''
                            THEN to_tsquery('simple', regexp_replace(
                                     websearch_to_tsquery('german', :query)::text,
                                     '''([^'']+)''',
                                     '''\\1'':*',
                                     'g'))
                       END AS pq
            ) q
            WHERE d.search_vector @@ q.pq
            ORDER BY ts_rank(d.search_vector, q.pq) DESC,
                     d.meta_date DESC NULLS LAST
            """)
    // Unpaged path — for bulk-edit "select all" and density chart
    List<UUID> findAllMatchingIdsByFts(@Param("query") String query);

    /**
     * Returns one page of FTS-ranked document IDs with the total match count.
     *
     * <p>Each row contains (in column order):
     * <ol>
     *   <li>UUID   — document id</li>
     *   <li>double — ts_rank score</li>
     *   <li>long   — COUNT(*) OVER () — full match count, not page count</li>
     * </ol>
     *
     * <p>Returns an empty list when the query matches no documents (including
     * stopword-only queries where websearch_to_tsquery returns an empty tsquery).
     * Use findAllMatchingIdsByFts for the unpaged bulk-edit path.
     */
    @Query(nativeQuery = true, value = """
            WITH q AS (
                SELECT CASE WHEN websearch_to_tsquery('german', :query)::text <> ''
                            THEN to_tsquery('simple', regexp_replace(
                                     websearch_to_tsquery('german', :query)::text,
                                     '''([^'']+)''',
                                     '''\\1'':*',
                                     'g'))
                       END AS pq
            ), matches AS (
                SELECT d.id, ts_rank(d.search_vector, q.pq) AS rank
                FROM documents d, q
                WHERE d.search_vector @@ q.pq
            )
            SELECT id, rank, COUNT(*) OVER () AS total
            FROM matches
            ORDER BY rank DESC, id
            OFFSET :offset LIMIT :limit
            """)
    List<Object[]> findFtsPageRaw(@Param("query") String query,
                                  @Param("offset") int offset,
                                  @Param("limit") int limit);

    /**
     * Returns match-enrichment data for a set of documents identified by their IDs.
     * Each row contains (in column order):
     * <ol>
     *   <li>UUID   — document id</li>
     *   <li>String — title headline with \x01/\x02 delimiters around matched terms</li>
     *   <li>String — best-ranked transcription snippet with \x01/\x02 delimiters, or null</li>
     *   <li>Boolean — whether the sender's name matched the query</li>
     *   <li>String — comma-separated matched receiver UUIDs, or null</li>
     *   <li>String — comma-separated matched tag UUIDs, or null</li>
     *   <li>String — summary snippet with \x01/\x02 delimiters, or null if summary didn't match</li>
     * </ol>
     * Short-circuit before calling this method when {@code ids} is empty or {@code query} is blank.
     */
    @Query(nativeQuery = true, value = """
            SELECT
                d.id,
                ts_headline('german', d.title, q.pq,
                    'StartSel=' || chr(1) || ',StopSel=' || chr(2) || ',HighlightAll=true')
                    AS title_headline,
                CASE WHEN best_block.text IS NOT NULL THEN
                    ts_headline('german', best_block.text, q.pq,
                        'StartSel=' || chr(1) || ',StopSel=' || chr(2) || ',MaxWords=50,MinWords=20')
                END AS transcription_snippet,
                (s.id IS NOT NULL AND
                 to_tsvector('german', COALESCE(s.first_name, '') || ' ' || COALESCE(s.last_name, ''))
                     @@ q.pq)
                    AS sender_matched,
                (SELECT string_agg(r.id::text, ',')
                 FROM document_receivers dr
                 JOIN persons r ON r.id = dr.person_id
                 WHERE dr.document_id = d.id
                   AND to_tsvector('german', COALESCE(r.first_name, '') || ' ' || r.last_name)
                           @@ q.pq
                ) AS matched_receiver_ids,
                (SELECT string_agg(t.id::text, ',')
                 FROM document_tags dt
                 JOIN tag t ON t.id = dt.tag_id
                 WHERE dt.document_id = d.id
                   AND to_tsvector('german', t.name) @@ q.pq
                ) AS matched_tag_ids,
                CASE WHEN d.summary IS NOT NULL AND d.summary <> ''
                          AND to_tsvector('german', d.summary) @@ q.pq
                     THEN ts_headline('german', d.summary, q.pq,
                              'StartSel=' || chr(1) || ',StopSel=' || chr(2) || ',MaxWords=50,MinWords=20')
                END AS summary_snippet
            FROM documents d
            CROSS JOIN LATERAL (
                SELECT CASE WHEN websearch_to_tsquery('german', :query)::text <> ''
                            THEN to_tsquery('simple', regexp_replace(
                                     websearch_to_tsquery('german', :query)::text,
                                     '''([^'']+)''',
                                     '''\\1'':*',
                                     'g'))
                       END AS pq
            ) q
            LEFT JOIN persons s ON s.id = d.sender_id
            LEFT JOIN LATERAL (
                SELECT tb.text
                FROM transcription_blocks tb
                WHERE tb.document_id = d.id
                  AND to_tsvector('german', tb.text) @@ q.pq
                ORDER BY ts_rank(to_tsvector('german', tb.text), q.pq) DESC
                LIMIT 1
            ) best_block ON true
            WHERE d.id IN :ids
            """)
    List<Object[]> findEnrichmentData(@Param("ids") Collection<UUID> ids, @Param("query") String query);

    // --- Mission Control Strip queues ---

    /** Documents with no annotations — Segmentierung column. */
    @Query(nativeQuery = true, value = """
            SELECT d.id, d.title, d.meta_date AS documentDate,
                   0 AS annotationCount, 0 AS textedBlockCount, 0 AS reviewedBlockCount
            FROM documents d
            WHERE d.status NOT IN ('PLACEHOLDER')
              AND NOT EXISTS (SELECT 1 FROM document_annotations da WHERE da.document_id = d.id)
            ORDER BY HASHTEXT(d.id::text || EXTRACT(WEEK FROM NOW())::int::text)
            LIMIT :limit
            """)
    List<TranscriptionQueueProjection> findSegmentationQueue(@Param("limit") int limit);

    /** Documents with annotations but not yet fully reviewed — Transkription column. */
    @Query(nativeQuery = true, value = """
            SELECT d.id, d.title, d.meta_date AS documentDate,
                   COUNT(DISTINCT da.id)                                                             AS annotationCount,
                   COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END)   AS textedBlockCount,
                   COUNT(DISTINCT CASE WHEN tb.reviewed = true THEN tb.id END)                      AS reviewedBlockCount
            FROM documents d
            JOIN document_annotations da ON da.document_id = d.id
            LEFT JOIN transcription_blocks tb ON tb.document_id = d.id
            GROUP BY d.id, d.title, d.meta_date
            HAVING COUNT(DISTINCT da.id) > 0
               AND (
                 COUNT(DISTINCT CASE WHEN tb.reviewed = true THEN tb.id END)::float /
                 COUNT(DISTINCT da.id)
               ) < 0.90
            ORDER BY COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END) DESC,
                     HASHTEXT(d.id::text || EXTRACT(WEEK FROM NOW())::int::text)
            LIMIT :limit
            """)
    List<TranscriptionQueueProjection> findTranscriptionQueue(@Param("limit") int limit);

    /** Documents with reviewed_pct >= 90 % — Lesefertig column. */
    @Query(nativeQuery = true, value = """
            SELECT d.id, d.title, d.meta_date AS documentDate,
                   COUNT(DISTINCT da.id)                                                             AS annotationCount,
                   COUNT(DISTINCT CASE WHEN tb.text IS NOT NULL AND tb.text <> '' THEN tb.id END)   AS textedBlockCount,
                   COUNT(DISTINCT CASE WHEN tb.reviewed = true THEN tb.id END)                      AS reviewedBlockCount
            FROM documents d
            JOIN document_annotations da ON da.document_id = d.id
            LEFT JOIN transcription_blocks tb ON tb.document_id = d.id
            GROUP BY d.id, d.title, d.meta_date
            HAVING COUNT(DISTINCT da.id) > 0
               AND (
                 COUNT(DISTINCT CASE WHEN tb.reviewed = true THEN tb.id END)::float /
                 COUNT(DISTINCT da.id)
               ) >= 0.90
            ORDER BY (
                COUNT(DISTINCT CASE WHEN tb.reviewed = true THEN tb.id END)::float /
                COUNT(DISTINCT da.id)
              ) DESC
            LIMIT :limit
            """)
    List<TranscriptionQueueProjection> findReadyToReadQueue(@Param("limit") int limit);

    /** Weekly pulse: distinct documents that received new work in each pipeline stage. */
    @Query(nativeQuery = true, value = """
            SELECT
              (SELECT COUNT(DISTINCT da.document_id) FROM document_annotations da
               WHERE da.created_at >= NOW() - INTERVAL '7 days')                             AS segmentationCount,
              (SELECT COUNT(DISTINCT tb.document_id) FROM transcription_blocks tb
               WHERE tb.created_at >= NOW() - INTERVAL '7 days'
                 AND tb.text IS NOT NULL AND tb.text <> '')                                  AS transcriptionCount
            """)
    TranscriptionWeeklyStatsProjection findWeeklyStats();

}