package org.raddatz.familienarchiv.geschichte;

import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.criteria.Subquery;
import org.raddatz.familienarchiv.person.Person;
import org.springframework.data.jpa.domain.Specification;

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

public final class GeschichteSpecifications {

    private GeschichteSpecifications() {}

    public static Specification<Geschichte> hasStatus(GeschichteStatus status) {
        return (root, query, cb) -> status == null ? null : cb.equal(root.get("status"), status);
    }

    /**
     * Adds {@code ORDER BY COALESCE(publishedAt, updatedAt) DESC} to the query without contributing
     * a predicate. Combined into the spec chain via {@code .and(...)}; the {@code conjunction}
     * acts as a no-op WHERE clause.
     */
    public static Specification<Geschichte> orderByDisplayDateDesc() {
        return (root, query, cb) -> {
            // Skip ordering on count queries — JPA forbids orderBy on COUNT projections.
            if (query != null
                    && Long.class != query.getResultType()
                    && long.class != query.getResultType()) {
                query.orderBy(cb.desc(cb.coalesce(root.get("publishedAt"), root.get("updatedAt"))));
            }
            return cb.conjunction();
        };
    }

    // null authorId → no restriction (PUBLISHED path passes null; Spring Data skips null predicates)
    public static Specification<Geschichte> hasAuthor(UUID authorId) {
        return (root, query, cb) ->
                authorId == null ? null : cb.equal(root.get("author").get("id"), authorId);
    }

    // TODO(lesereisen-editor): restore document filter via journey_items join when editor lands

    /**
     * AND-filter across persons: the Geschichte must be associated with EVERY id in {@code personIds}.
     *
     * <p>Implemented as one EXISTS subquery per id (canonical Criteria-API idiom for AND across a
     * many-to-many join). Mirrors {@link DocumentSpecifications#hasTags} which uses the same shape.
     * Empty / null input returns {@code null} (i.e. no constraint added).
     */
    public static Specification<Geschichte> hasAllPersons(Collection<UUID> personIds) {
        return (root, query, cb) -> {
            if (personIds == null || personIds.isEmpty()) return null;
            List<Predicate> predicates = new ArrayList<>(personIds.size());
            for (UUID id : personIds) {
                predicates.add(cb.exists(personSubquery(root, query, cb, id)));
            }
            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }

    private static Subquery<UUID> personSubquery(
            Root<Geschichte> root, CriteriaQuery<?> query, CriteriaBuilder cb, UUID personId) {
        Subquery<UUID> sub = query.subquery(UUID.class);
        Root<Geschichte> subRoot = sub.from(Geschichte.class);
        Join<Geschichte, Person> persons = subRoot.join("persons");
        sub.select(subRoot.get("id"))
                .where(cb.equal(subRoot.get("id"), root.get("id")),
                        cb.equal(persons.get("id"), personId));
        return sub;
    }

}
