package org.raddatz.familienarchiv.document;

import jakarta.persistence.criteria.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import org.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.document.DocumentStatus;
import org.raddatz.familienarchiv.tag.Tag;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;

public class DocumentSpecifications {

    // Filtert nach einer vorberechneten ID-Liste (aus FTS-Abfrage)
    public static Specification<Document> hasIds(List<UUID> ids) {
        return (root, query, cb) -> {
            if (ids == null || ids.isEmpty()) return cb.disjunction();
            return root.get("id").in(ids);
        };
    }

    // Filtert nach Sender (für Schriftwechsel wichtig)
    public static Specification<Document> hasSender(UUID personId) {
        return (root, query, cb) -> personId == null ? null : cb.equal(root.get("sender").get("id"), personId);
    }

    // Filtert nach Empfänger (nutzt Join)
    public static Specification<Document> hasReceiver(UUID personId) {
        return (root, query, cb) -> {
            if (personId == null)
                return null;
            return cb.equal(root.join("receivers").get("id"), personId);
        };
    }

    // Filtert nach Zeitraum
    public static Specification<Document> isBetween(LocalDate start, LocalDate end) {
        return (root, query, cb) -> {
            if (start == null && end == null)
                return null;
            if (start != null && end != null)
                return cb.between(root.get("documentDate"), start, end);
            if (start != null)
                return cb.greaterThanOrEqualTo(root.get("documentDate"), start);
            return cb.lessThanOrEqualTo(root.get("documentDate"), end);
        };
    }

    // Filtert nach Status
    public static Specification<Document> hasStatus(DocumentStatus status) {
        return (root, query, cb) -> status == null ? null : cb.equal(root.get("status"), status);
    }

    // Filtert auf undatierte Dokumente (meta_date IS NULL) — für die "Nur undatierte"-Triage.
    // false → kein Prädikat (no-op), true → documentDate IS NULL (issue #668).
    public static Specification<Document> undatedOnly(boolean undated) {
        return (root, query, cb) -> undated ? cb.isNull(root.get("documentDate")) : null;
    }

    /**
     * Filtert nach vorausgeweiteten Tag-ID-Sets mit AND- oder OR-Logik.
     *
     * <p>AND (useOr=false): Das Dokument muss mindestens einen Tag aus <em>jedem</em> Set besitzen.
     * <p>OR (useOr=true): Das Dokument muss mindestens einen Tag aus der Vereinigung aller Sets besitzen.
     *
     * <p>Jedes Set repräsentiert einen ausgewählten Tag inklusive aller seiner Nachkommen
     * (vorausgeweitet durch {@code TagRepository.findDescendantIdsByName}).
     */
    public static Specification<Document> hasTags(List<Set<UUID>> tagIdSets, boolean useOr) {
        return (root, query, cb) -> {
            if (tagIdSets == null || tagIdSets.isEmpty())
                return null;

            if (!useOr) {
                // AND mode: an empty set means the tag resolved to no IDs (doesn't exist) —
                // no document can satisfy the condition, so return no results immediately.
                boolean hasEmptySet = tagIdSets.stream().anyMatch(s -> s == null || s.isEmpty());
                if (hasEmptySet) return cb.disjunction();
            }

            List<Set<UUID>> nonEmpty = tagIdSets.stream()
                    .filter(s -> s != null && !s.isEmpty())
                    .toList();
            if (nonEmpty.isEmpty()) return null;

            if (useOr) {
                Set<UUID> union = new java.util.HashSet<>();
                nonEmpty.forEach(union::addAll);
                return documentHasTagIn(root, query, cb, union);
            }

            // AND: one EXISTS subquery per set
            List<Predicate> predicates = new ArrayList<>();
            for (Set<UUID> ids : nonEmpty) {
                predicates.add(documentHasTagIn(root, query, cb, ids));
            }
            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }

    private static Predicate documentHasTagIn(
            Root<Document> root,
            jakarta.persistence.criteria.CriteriaQuery<?> query,
            jakarta.persistence.criteria.CriteriaBuilder cb,
            Set<UUID> tagIds) {
        Subquery<UUID> subquery = query.subquery(UUID.class);
        Root<Document> subRoot = subquery.from(Document.class);
        Join<Document, Tag> subTags = subRoot.join("tags");

        subquery.select(subRoot.get("id"))
                .where(
                        cb.equal(subRoot.get("id"), root.get("id")),
                        subTags.get("id").in(tagIds)
                );
        return cb.exists(subquery);
    }

    // Filtert nach partiellem Tag-Namen (ILIKE) — für Live-Tag-Suche
    public static Specification<Document> hasTagPartial(String tagQ) {
        return (root, query, cb) -> {
            if (!StringUtils.hasText(tagQ))
                return null;
            String likePattern = "%" + tagQ.toLowerCase() + "%";

            Subquery<Long> subquery = query.subquery(Long.class);
            Root<Document> subRoot = subquery.from(Document.class);
            Join<Document, Tag> tagJoin = subRoot.join("tags");

            subquery.select(cb.literal(1L))
                    .where(
                            cb.equal(subRoot.get("id"), root.get("id")),
                            cb.like(cb.lower(tagJoin.get("name")), likePattern)
                    );

            return cb.exists(subquery);
        };
    }

}
