package com.infoclinika.pdx.specification;

import com.infoclinika.pdx.domain.sample.Sample;
import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.Join;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import static com.google.common.collect.Lists.newArrayList;
import static com.infoclinika.pdx.domain.experiment.expression.ExpressionType.COMBAT;
import static com.infoclinika.pdx.domain.sample.SampleType.PDX;
import static com.infoclinika.pdx.security.AuthenticationProvider.getCurrentUserId;
import static com.infoclinika.pdx.security.AuthenticationProvider.userLoggedIn;
import static com.infoclinika.pdx.specification.SpecificationUtils.getPath;
import static com.infoclinika.pdx.specification.SpecificationUtils.joinWithAnd;
import static com.infoclinika.pdx.specification.SpecificationUtils.joinWithOr;
import static java.util.stream.Collectors.toList;

/**
 * Utility methods to create common specifications.
 */
public final class Specifications {
    private static final String SAMPLE_KEY = "sample";
    private static final String TYPE_KEY = "type";

    private static final String SAMPLE_TYPE_KEY = "sample.type";
    private static final String SUB_TYPE_KEY = "subType";
    private static final String PRIMARY_SITE_ID_KEY = "primarySite.id";
    private static final String PROJECT_ID_KEY = "project.id";

    private static final String IS_PUBLIC_KEY = "isPublic";
    private static final String SAMPLE_OWNER_ID_KEY = "sample.owner.id";

    private static final String IN_OPERATION = ":";

    private Specifications() {
    }

    /**
     * Creates specification to include PDX samples only with COMBAT normalization type.
     *
     * @param prefix Filter query prefix.
     * @param <T>    Specification root class.
     * @return Specification to include PDX samples with COMBAT type.
     */
    public static <T> Specification<T> getSpecificationWithPdxCombat(String prefix) {
        final String sampleType = prefix.concat(SAMPLE_TYPE_KEY);
        final String rnaSeqSubType = prefix.concat(SUB_TYPE_KEY);

        final Specification<T> isPdxSample = (root, query1, criteriaBuilder) ->
            criteriaBuilder.equal(getPath(root, sampleType), PDX);
        final Specification<T> isCombat = (root, query1, criteriaBuilder) ->
            criteriaBuilder.equal(getPath(root, rnaSeqSubType), COMBAT);
        final Specification<T> isNotPdxSample = (root, query1, criteriaBuilder) ->
            criteriaBuilder.notEqual(getPath(root, sampleType), PDX);

        final Specification<T> subTypeSpec = isNotPdxSample.or(isPdxSample.and(isCombat));

        if (userLoggedIn()) {
            final Specification<T> userAccessSpec = (root, query, builder) ->  {
                final Path<Object> path = getOwnerIdPath(root, SAMPLE_OWNER_ID_KEY);
                return builder.equal(path, getCurrentUserId());
            };

            return subTypeSpec.or(userAccessSpec);
        }

        return subTypeSpec;
    }

    /**
     * Creates specification for samples that contain records for provided genomic features.
     *
     * @param resultClass Specification root class.
     * @param features    List of genomic features.
     * @param <T>         Specification root class.
     * @return Specification for samples that contain records for provided genomic features.
     */
    public static <T> Specification<Sample> getSpecificationWithGenomicFeatures(Class<T> resultClass,
                                                                                String... features) {
        return (root, q, builder) -> {
            final List<String> featureList = newArrayList(features);
            final Subquery<Sample> subQuery = q.subquery(Sample.class);
            final Root<T> result = subQuery.from(resultClass);
            final Join<T, Sample> join = result.join(SAMPLE_KEY);
            final Predicate featurePredicate = result.get(TYPE_KEY).in(featureList);

            final Specification<T> accessSpecification = getUserAccessSpecification(SAMPLE_OWNER_ID_KEY);
            final Predicate accessPredicate = accessSpecification.toPredicate(result, q, builder);

            subQuery.select(join).where(builder.and(featurePredicate, accessPredicate));
            return builder.in(root).value(subQuery);
        };
    }

    /**
     * Joins filter queries for primary sites into single specification.
     *
     * @param queries List of filter queries.
     * @param prefix  Filter query prefix.
     * @param <T>     Specification root class.
     * @return Specification for primary site filter options.
     */
    public static <T> Specification<T> getPrimarySiteSpecification(List<FilterQuery> queries, String prefix) {
        final List<FilterQuery> siteQueries = queries.stream()
                                                     .filter(query -> query.containsParam(PRIMARY_SITE_ID_KEY,
                                                                                          IN_OPERATION))
                                                     .map(query -> query.withKeyPrefix(prefix))
                                                     .collect(toList());
        return joinWithOr(getSpecifications(siteQueries));
    }

    /**
     * Joins filter queries for sample types into single specification.
     *
     * @param queries List of filter queries.
     * @param prefix  Filter query prefix.
     * @param <T>     Specification root class.
     * @return Specification for sample type filter options.
     */
    public static <T> Specification<T> getSampleTypeSpecification(List<FilterQuery> queries, String prefix) {
        final List<FilterQuery> typeQueries = queries.stream()
                                                     .filter(query -> query.containsParam(TYPE_KEY))
                                                     .map(query -> query.withKeyPrefix(prefix))
                                                     .collect(toList());
        return joinWithOr(getSpecifications(typeQueries));
    }

    /**
     * Joins filter queries for general options (excluding primary site and sample type queries)
     * into single specification.
     *
     * @param queries List of filter queries.
     * @param prefix  Filter query prefix.
     * @param <T>     Specification root class.
     * @return Specification for general filter options.
     */
    public static <T> Specification<T> getGeneralOptionsSpecification(List<FilterQuery> queries, String prefix) {
        final List<FilterQuery> generalQueries =
            queries.stream()
                   .filter(query -> !query.containsParam(PRIMARY_SITE_ID_KEY, IN_OPERATION))
                   .filter(query -> !query.containsParam(TYPE_KEY))
                   .map(query -> query.replaceKey(PRIMARY_SITE_ID_KEY, prefix.concat(PRIMARY_SITE_ID_KEY))
                                      .replaceKey(PROJECT_ID_KEY, prefix.concat(PROJECT_ID_KEY)))
                   .collect(toList());
        return joinWithAnd(getSpecifications(generalQueries));
    }

    /**
     * Joins provided filter queries into a common specification with all filter options.
     *
     * @param queries List of filter queries.
     * @param prefix  Filter query prefix.
     * @param <T>     Specification root class.
     * @return Common specification with all filter options.
     */
    public static <T> Specification<T> getCommonSpecification(List<FilterQuery> queries, String prefix) {
        final List<Specification<T>> specs = new ArrayList<>();
        specs.add(getSampleTypeSpecification(queries, prefix));
        specs.add(getPrimarySiteSpecification(queries, prefix));
        specs.add(getGeneralOptionsSpecification(queries, prefix));

        return joinWithAnd(specs);
    }

    /**
     * Creates specification to get public data and data for current user.
     *
     * @param ownerPrefix Prefix of owner query.
     * @param <T> Specification root class.
     * @return Specification with data access.
     */
    public static <T> Specification<T> getUserAccessSpecification(String ownerPrefix) {
        return (root, query, builder) -> {
            final Predicate publicDataPredicate = builder.isTrue(root.get(IS_PUBLIC_KEY));

            if (userLoggedIn()) {
                final Path<Object> path = getOwnerIdPath(root, ownerPrefix);
                final Predicate privateDataPredicate = builder.equal(path, getCurrentUserId());
                return builder.or(publicDataPredicate, privateDataPredicate);
            }

            return publicDataPredicate;
        };
    }

    private static <T> Path<Object> getOwnerIdPath(Root<T> root, String ownerPrefix) {
        final String[] keys = ownerPrefix.split("\\.");
        Path<Object> path = root.get(keys[0]);

        for (int i = 1; i < keys.length; i++) {
            path = path.get(keys[i]);
        }
        return path;
    }

    private static <T> List<Specification<T>> getSpecifications(List<FilterQuery> queries) {
        return queries.stream()
                      .map(query -> new SpecificationBuilder<T>().build(query))
                      .filter(Objects::nonNull)
                      .collect(toList());
    }
}
