package org.raddatz.familienarchiv.person;

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

import org.raddatz.familienarchiv.person.PersonSummaryDTO;
import org.raddatz.familienarchiv.person.Person;
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;

@Repository
public interface PersonRepository extends JpaRepository<Person, UUID> {

    @Query("SELECT DISTINCT p FROM Person p LEFT JOIN p.nameAliases a WHERE " +
           "LOWER(CONCAT(COALESCE(p.firstName, ''),' ',p.lastName)) LIKE LOWER(CONCAT('%', :query, '%')) OR " +
           "LOWER(CONCAT(p.lastName, ' ', COALESCE(p.firstName, ''))) LIKE LOWER(CONCAT('%', :query, '%')) OR " +
           "LOWER(p.alias) LIKE LOWER(CONCAT('%', :query, '%')) OR " +
           "LOWER(a.lastName) LIKE LOWER(CONCAT('%', :query, '%')) OR " +
           "LOWER(a.firstName) LIKE LOWER(CONCAT('%', :query, '%')) " +
           "ORDER BY p.lastName ASC, p.firstName ASC")
    List<Person> searchByName(@Param("query") String query);

    // Hilfsmethode: Alle sortiert laden (für den leeren Status)
    List<Person> findAllByOrderByLastNameAscFirstNameAsc();

    // Stammbaum-Knoten: alle Personen mit family_member = true.
    List<Person> findByFamilyMemberTrueOrderByLastNameAscFirstNameAsc();

    // Exact-case alias lookup — the first resolution step in findOrCreateByAlias.
    // Case-colliding aliases across persons (müller / Müller) are valid human labels, NOT
    // duplicates: source_ref is the stable identity (ADR-025/033), alias is editable. Do NOT
    // add a unique(lower(alias)) constraint — see ADR-033.
    Optional<Person> findByAlias(String alias);

    // Plural case-insensitive alias lookup — the fallback step. Returns ALL case-folding
    // siblings so the service can pick a deterministic one (lowest id) instead of letting a
    // derived Optional<…>IgnoreCase throw NonUniqueResultException. See ADR-033.
    List<Person> findAllByAliasIgnoreCase(String alias);

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

    // Exact-case first+last name match — the first step of filename-based sender resolution.
    // Explicit `=` (HQL, not a derived query) so a null firstName binds as `first_name = NULL`
    // — never a match — instead of the derived-query fold to `first_name IS NULL`, which would
    // pull a last-name-only row in as a sender (a provenance defect). See ADR-033.
    @Query("SELECT p FROM Person p WHERE p.firstName = :firstName AND p.lastName = :lastName")
    Optional<Person> findByFirstNameAndLastName(@Param("firstName") String firstName,
                                                @Param("lastName") String lastName);

    // Plural case-insensitive first+last name match — lets findByName bail to empty on 2+ matches
    // instead of letting a derived Optional<…>IgnoreCase throw NonUniqueResultException. Same
    // null fail-closed guarantee as above: LOWER(:firstName) is NULL for a null arg, so a null
    // first name resolves to no match (not first_name IS NULL widening). See ADR-033.
    @Query("SELECT p FROM Person p WHERE LOWER(p.firstName) = LOWER(:firstName) "
         + "AND LOWER(p.lastName) = LOWER(:lastName)")
    List<Person> findAllByFirstNameIgnoreCaseAndLastNameIgnoreCase(@Param("firstName") String firstName,
                                                                   @Param("lastName") String lastName);

    // --- PersonSummaryDTO with document count ---

    @Query(value = """
            SELECT p.id, p.title, p.first_name AS firstName, p.last_name AS lastName,
                   p.person_type AS personType,
                   p.alias, CAST(EXTRACT(YEAR FROM p.birth_date) AS int) AS birthYear,
                   CAST(EXTRACT(YEAR FROM p.death_date) AS int) AS deathYear,
                   p.birth_date AS birthDate, p.birth_date_precision AS birthDatePrecision,
                   p.death_date AS deathDate, p.death_date_precision AS deathDatePrecision, p.notes,
                   p.family_member AS familyMember, p.provisional AS provisional,
                   (SELECT COUNT(*) FROM documents d WHERE d.sender_id = p.id)
                   + (SELECT COUNT(*) FROM document_receivers dr WHERE dr.person_id = p.id) AS documentCount
            FROM persons p
            ORDER BY p.last_name ASC, p.first_name ASC
            """,
            nativeQuery = true)
    List<PersonSummaryDTO> findAllWithDocumentCount();

    @Query(value = """
            SELECT p.id, p.title, p.first_name AS firstName, p.last_name AS lastName,
                   p.person_type AS personType,
                   p.alias, CAST(EXTRACT(YEAR FROM p.birth_date) AS int) AS birthYear,
                   CAST(EXTRACT(YEAR FROM p.death_date) AS int) AS deathYear,
                   p.birth_date AS birthDate, p.birth_date_precision AS birthDatePrecision,
                   p.death_date AS deathDate, p.death_date_precision AS deathDatePrecision, p.notes,
                   p.family_member AS familyMember, p.provisional AS provisional,
                   (SELECT COUNT(*) FROM documents d WHERE d.sender_id = p.id)
                   + (SELECT COUNT(*) FROM document_receivers dr WHERE dr.person_id = p.id) AS documentCount
            FROM persons p
            LEFT JOIN person_name_aliases a ON a.person_id = p.id
            WHERE LOWER(CONCAT(COALESCE(p.first_name,''),' ',p.last_name)) LIKE LOWER(CONCAT('%',:query,'%'))
               OR LOWER(CONCAT(p.last_name,' ',COALESCE(p.first_name,''))) LIKE LOWER(CONCAT('%',:query,'%'))
               OR LOWER(p.alias) LIKE LOWER(CONCAT('%',:query,'%'))
               OR LOWER(a.last_name) LIKE LOWER(CONCAT('%',:query,'%'))
            GROUP BY p.id, p.title, p.first_name, p.last_name, p.person_type, p.alias, p.birth_date, p.birth_date_precision, p.death_date, p.death_date_precision, p.notes, p.family_member, p.provisional
            ORDER BY p.last_name ASC, p.first_name ASC
            """,
            nativeQuery = true)
    List<PersonSummaryDTO> searchWithDocumentCount(@Param("query") String query);

    // ORDER BY uses the computed alias "documentCount" — valid PostgreSQL (aliases allowed in ORDER BY,
    // unlike WHERE/HAVING). This is intentional; it would silently fail on MySQL or H2.
    @Query(value = """
            SELECT p.id, p.title, p.first_name AS firstName, p.last_name AS lastName,
                   p.person_type AS personType,
                   p.alias, CAST(EXTRACT(YEAR FROM p.birth_date) AS int) AS birthYear,
                   CAST(EXTRACT(YEAR FROM p.death_date) AS int) AS deathYear,
                   p.birth_date AS birthDate, p.birth_date_precision AS birthDatePrecision,
                   p.death_date AS deathDate, p.death_date_precision AS deathDatePrecision, p.notes,
                   p.family_member AS familyMember, p.provisional AS provisional,
                   (SELECT COUNT(*) FROM documents d WHERE d.sender_id = p.id)
                   + (SELECT COUNT(*) FROM document_receivers dr WHERE dr.person_id = p.id) AS documentCount
            FROM persons p
            ORDER BY documentCount DESC
            LIMIT :limit
            """,
            nativeQuery = true)
    List<PersonSummaryDTO> findTopByDocumentCount(@Param("limit") int limit);

    // --- #667: filter-aware paged directory ---
    //
    // The slice query and the count query below MUST keep an IDENTICAL WHERE clause so the
    // rendered page and totalElements can never drift. Every filter is nullable: a null param
    // disables that predicate via the `:param IS NULL OR …` idiom. `readerDefault` (a plain
    // boolean) restricts to "familyMember OR has documents"; the explicit filters AND on top.
    // documentCount is recomputed inline (not via the SELECT alias) because WHERE cannot
    // reference a computed alias. All params are named — no string concatenation, no injection.
    String FILTER_WHERE = """
            WHERE (CAST(:type AS text) IS NULL OR p.person_type = CAST(:type AS text))
              AND (:familyOnly = FALSE OR :familyOnly IS NULL OR p.family_member = TRUE)
              AND (:hasDocuments = FALSE OR :hasDocuments IS NULL OR (
                    (SELECT COUNT(*) FROM documents d WHERE d.sender_id = p.id)
                    + (SELECT COUNT(*) FROM document_receivers dr WHERE dr.person_id = p.id)) > 0)
              AND (:provisional IS NULL OR p.provisional = :provisional)
              AND (:readerDefault = FALSE OR (
                    p.family_member = TRUE OR (
                    (SELECT COUNT(*) FROM documents d WHERE d.sender_id = p.id)
                    + (SELECT COUNT(*) FROM document_receivers dr WHERE dr.person_id = p.id)) > 0))
              AND (CAST(:query AS text) IS NULL OR
                    LOWER(CONCAT(COALESCE(p.first_name,''),' ',p.last_name)) LIKE LOWER(CONCAT('%',CAST(:query AS text),'%'))
                 OR LOWER(CONCAT(p.last_name,' ',COALESCE(p.first_name,''))) LIKE LOWER(CONCAT('%',CAST(:query AS text),'%'))
                 OR LOWER(p.alias) LIKE LOWER(CONCAT('%',CAST(:query AS text),'%')))
            """;

    @Query(value = """
            SELECT p.id, p.title, p.first_name AS firstName, p.last_name AS lastName,
                   p.person_type AS personType,
                   p.alias, CAST(EXTRACT(YEAR FROM p.birth_date) AS int) AS birthYear,
                   CAST(EXTRACT(YEAR FROM p.death_date) AS int) AS deathYear,
                   p.birth_date AS birthDate, p.birth_date_precision AS birthDatePrecision,
                   p.death_date AS deathDate, p.death_date_precision AS deathDatePrecision, p.notes,
                   p.family_member AS familyMember, p.provisional AS provisional,
                   (SELECT COUNT(*) FROM documents d WHERE d.sender_id = p.id)
                   + (SELECT COUNT(*) FROM document_receivers dr WHERE dr.person_id = p.id) AS documentCount
            FROM persons p
            """ + FILTER_WHERE + """
            ORDER BY p.last_name ASC, p.first_name ASC
            LIMIT :limit OFFSET :offset
            """,
            nativeQuery = true)
    List<PersonSummaryDTO> findByFilter(@Param("type") String type,
                                        @Param("familyOnly") Boolean familyOnly,
                                        @Param("hasDocuments") Boolean hasDocuments,
                                        @Param("provisional") Boolean provisional,
                                        @Param("readerDefault") boolean readerDefault,
                                        @Param("query") String query,
                                        @Param("limit") int limit,
                                        @Param("offset") int offset);

    @Query(value = "SELECT COUNT(*) FROM persons p " + FILTER_WHERE, nativeQuery = true)
    long countByFilter(@Param("type") String type,
                       @Param("familyOnly") Boolean familyOnly,
                       @Param("hasDocuments") Boolean hasDocuments,
                       @Param("provisional") Boolean provisional,
                       @Param("readerDefault") boolean readerDefault,
                       @Param("query") String query);

    // --- Correspondent queries ---

    @Query(value = """
            SELECT p.* FROM persons p
            INNER JOIN (
                SELECT dr.person_id AS other_id, d.id AS doc_id
                FROM documents d
                JOIN document_receivers dr ON dr.document_id = d.id
                WHERE d.sender_id = :personId
                UNION ALL
                SELECT d.sender_id AS other_id, d.id AS doc_id
                FROM documents d
                JOIN document_receivers dr ON dr.document_id = d.id
                WHERE dr.person_id = :personId AND d.sender_id IS NOT NULL
            ) shared ON shared.other_id = p.id
            WHERE p.id != :personId
            GROUP BY p.id
            ORDER BY COUNT(DISTINCT shared.doc_id) DESC
            LIMIT 10
            """, nativeQuery = true)
    List<Person> findCorrespondents(@Param("personId") UUID personId);

    @Query(value = """
            SELECT p.* FROM persons p
            INNER JOIN (
                SELECT dr.person_id AS other_id, d.id AS doc_id
                FROM documents d
                JOIN document_receivers dr ON dr.document_id = d.id
                WHERE d.sender_id = :personId
                UNION ALL
                SELECT d.sender_id AS other_id, d.id AS doc_id
                FROM documents d
                JOIN document_receivers dr ON dr.document_id = d.id
                WHERE dr.person_id = :personId AND d.sender_id IS NOT NULL
            ) shared ON shared.other_id = p.id
            WHERE p.id != :personId
            AND (LOWER(CONCAT(COALESCE(p.first_name,''),' ',p.last_name)) LIKE LOWER(CONCAT('%',:q,'%'))
                 OR LOWER(CONCAT(p.last_name,' ',COALESCE(p.first_name,''))) LIKE LOWER(CONCAT('%',:q,'%'))
                 OR LOWER(p.alias) LIKE LOWER(CONCAT('%',:q,'%')))
            GROUP BY p.id
            ORDER BY COUNT(DISTINCT shared.doc_id) DESC
            LIMIT 10
            """, nativeQuery = true)
    List<Person> findCorrespondentsWithFilter(@Param("personId") UUID personId, @Param("q") String q);

    // --- Merge helpers (native SQL to bypass JPA entity layer) ---
    // clearAutomatically + flushAutomatically keep the L1 cache from desyncing: these bulk
    // updates run beneath Hibernate, and mergePersons follows them with a deleteById whose
    // ON DELETE CASCADE (V71) also fires beneath the session.

    @Modifying(clearAutomatically = true, flushAutomatically = true)
    @Query(value = "UPDATE documents SET sender_id = :target WHERE sender_id = :source", nativeQuery = true)
    void reassignSender(@Param("source") UUID source, @Param("target") UUID target);

    @Modifying(clearAutomatically = true, flushAutomatically = true)
    @Query(value = """
            INSERT INTO document_receivers (document_id, person_id)
            SELECT document_id, :target FROM document_receivers
            WHERE person_id = :source
              AND document_id NOT IN (
                  SELECT document_id FROM document_receivers WHERE person_id = :target
              )
            """, nativeQuery = true)
    void insertMissingReceiverReference(@Param("source") UUID source, @Param("target") UUID target);

    // Boxed Integer — matches the nullable person.generation column (primitive int would reject null rows).
    List<Person> findByGeneration(Integer generation);
}
