package com.epimorphics.registry.store;

import com.epimorphics.appbase.webapi.WebApiException;
import com.epimorphics.registry.core.Description;
import com.epimorphics.registry.core.RegisterItem;
import org.apache.commons.lang.text.StrBuilder;
import org.apache.jena.query.ParameterizedSparqlString;
import org.apache.jena.query.Query;
import org.apache.jena.query.QueryFactory;
import org.apache.jena.rdf.model.*;
import org.apache.jena.riot.system.IRIResolver;
import org.apache.jena.sparql.path.Path;
import org.apache.jena.sparql.path.PathFactory;

import javax.ws.rs.core.Response;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

import static java.util.stream.Collectors.joining;

/**
 * Instantiates SPARQL queries that locate and classify registry entries which are
 * similar to a given set of resources, based on their indexed text properties.
 */
class FindSimilar {

    /**
     * Instantiate this class.
     * @param store A store which the queries generated by the returned instance should be run against.
     * @return A new instance.
     */
    static FindSimilar newInstance(Store store) {
        return new FindSimilar(store.getIndexedProperties());
    }

    private final List<Property> indexProps;
    private final Path textPath;

    private FindSimilar(List<Property> indexProps) {
        this.indexProps = indexProps;
        this.textPath = getTextPath(indexProps);
    }

    private Path getTextPath(List<Property> indexProps) {
        return indexProps.stream()
                .map(Property::asNode)
                .map(PathFactory::pathLink)
                .reduce(PathFactory::pathAlt).orElseThrow(() ->
                        new WebApiException(Response.Status.BAD_REQUEST, "Unable to find similar entries: a text index is required!")
                );
    }

    private String getValuesRow(RegisterItem item, String text, Double similarity) {
        String uri = item.getRoot().getURI();
        if (IRIResolver.checkIRI(uri)) {
            throw new WebApiException(Response.Status.BAD_REQUEST, "Unable to find similar entries: Resource URI " + uri + " is not valid.");
        }

        String query = Arrays.stream(text.split(" "))
                .map(word -> word + "~" + String.format("%.1f", similarity))
                .collect(joining(" AND ", "(", ")"));

        ParameterizedSparqlString pss = new ParameterizedSparqlString("( ?resource ?query ?text ?isRegister )");
        pss.setIri("resource", uri);
        pss.setLiteral("query", query);
        pss.setLiteral("text", text);
        pss.setLiteral("isRegister", item.isRegister());

        return pss.toString();
    }

    private Resource getResource(Description desc) {
        if (desc.isRegisterItem()) {
            return desc.asRegisterItem().getEntity();
        } else {
            return desc.asRegister().getRegister();
        }
    }

    private Stream<String> getValuesRows(Collection<RegisterItem> items, Double similarity) {
        return items.stream().flatMap(item -> {
            Resource resource = getResource(item);
            Stream<Statement> statements = indexProps.stream().map(resource::getProperty).filter(Objects::nonNull);
            return statements
                    .map(Statement::getObject)
                    .filter(RDFNode::isLiteral)
                    .map(RDFNode::asLiteral)
                    .map(Literal::getLexicalForm)
                    .map(text -> getValuesRow(item, text, similarity));
        });
    }

    /**
     * Build a query which locates registry entries similar to the given resources, based on their indexed text properties.
     * The resources may already be in the store or not. If they are, pass 'true' to the [withEdits] parameter.
     * The indexed properties will depend on the store that is being queried.
     * The query will only compare the given resources to the current versions of non-retired entries.
     *
     * The query returns the following result columns:
     * resource (Resource): Corresponds to one of the given resources.
     * similar (Resource): A registry entry which is considered similar to the resource.
     * isMatch (Boolean): Determines whether the original and similar resource have text property values which match exactly (case insensitive).
     *
     * There should not be any two results with the same value for "resource" and "similar".
     *
     * @throws WebApiException When no text index is defined.
     * @throws WebApiException When a resource has an invalid URI.
     * @param items     The registers and register items to compare against the rest of the registry.
     * @param withEdits Determines whether the given resources should be treated as updates to existing entities.
     * @param similarity An optional value between 0 and 1 which determines the required similarity between strings.
     *                   0 is the least similarity, 1 is the most.
     *                   See https://lucene.apache.org/core/2_9_4/queryparsersyntax.html#Fuzzy%20Searches.
     * @return A valid SPARQL query with result columns: [resource (Resource) , similar (Resource), isMatch (Boolean)].
     */
    public Query build(Collection<RegisterItem> items, Boolean withEdits, Double similarity) {
        Stream<String> valuesRows = getValuesRows(items, similarity);
        String raw = new StrBuilder()
                .appendln("PREFIX text:    <http://jena.apache.org/text#>")
                .appendln("PREFIX reg:     <http://purl.org/linked-data/registry#>")
                .appendln("PREFIX version: <http://purl.org/linked-data/version#>")
                .appendln("SELECT DISTINCT ?resource ?similar ?isMatch")
                .appendln("WHERE {")
                .appendln("  VALUES (?resource ?query ?text ?isRegister) {")
                .appendln(valuesRows.collect(joining("\n")))
                .appendln("  }")
                .appendln("  ?entity text:query ?query .")
                .appendln(withEdits ? "  FILTER(?entity != ?resource)" : "")
                .appendln("  OPTIONAL {")
                .appendln("    ?entity " + textPath + " ?match")
                .appendln("    FILTER(LCASE(STR(?match)) = LCASE(?text))")
                .appendln("  }")
                .appendln("  BIND(BOUND(?match) AS ?isMatch)")
                .appendln("  ?version reg:definition/reg:entity/version:currentVersion? ?entity .")
                .appendln("  ?version reg:status ?status .")
                .appendln("  ?similar version:currentVersion ?version .")
                .appendln("  FILTER(?status != reg:statusInvalid)")
                .appendln("  FILTER ( EXISTS { ?similar  reg:itemClass  reg:Register } = ?isRegister )")
                .appendln("}")
                .toString();

        return QueryFactory.create(raw);
    }
}

@@ -3208,16 +3208,37 @@
 stream()
+%0A                    
 .map(res
@@ -3248,18 +3248,19 @@
 ce::
-ge
+lis
 tPropert
 y).f
@@ -3255,18 +3255,131 @@
 tPropert
-y)
+ies)%0A                    .map(StmtIterator::toList)%0A                    .flatMap(List::stream)%0A                    
 .filter(
@@ -3397,16 +3397,17 @@
 nNull);%0A
+%0A
         

ADD Support for multi valued text properties