xquery version "3.1";
(:~
 : This is the module preparing the SearchAPI as described
 : at https://subugoe.pages.gwdg.de/ahiqar/api-documentation/
 : @version 0.1.0
 : @since 6.3.0
 :)

module namespace search="http://ahiqar.uni-goettingen.de/ns/search";

declare namespace exist="http://exist.sourceforge.net/NS/exist";
declare namespace output="http://www.w3.org/2010/xslt-xquery-serialization";
declare namespace tei="http://www.tei-c.org/ns/1.0";

import module namespace commons="http://ahiqar.uni-goettingen.de/ns/commons" at "commons.xqm";
import module namespace kwic = "http://exist-db.org/xquery/kwic" at "resource:org/exist/xquery/lib/kwic.xql";
import module namespace rest="http://exquery.org/ns/restxq";
import module namespace tapi-mani="http://ahiqar.uni-goettingen.de/ns/tapi/manifest" at "tapi-manifest.xqm";


declare
    %rest:HEAD
    %rest:POST("{$body}")
    %rest:path("/search")
    %output:method("json")
    %rest:consumes("application/json")
    %rest:produces("application/json")
function search:main($body)
as item()+ {
    (commons:get-response-header(200),
    search:perform(util:base64-decode($body) => parse-json())
)};

declare function local:validate-query($query as xs:string)
as xs:boolean {
    if(string-length($query) eq 0) then
        error(QName("search", "empty-query"), "got empty query string")
    else if(contains($query, "script")) then
        error(QName("search", "script"), "query contains not allowed string: script")
    else
        true()
};

declare %private function local:get-language-collection-by-uri($teiUri as xs:string)
as xs:string? {
    let $langAggregation := commons:get-parent-uri($teiUri) => commons:get-parent-uri() => commons:get-resource-information()
    return
        $langAggregation("title")
};

declare %private function local:get-index($urlSuffix as xs:string, $pathTo as xs:string)
as xs:nonNegativeInteger {
    let $request := <hc:request href="http://localhost:8080/exist/restxq/textapi/ahiqar/{$urlSuffix}" method="get" />
    let $map := hc:send-request($request)[2] => util:binary-to-string() => parse-json()
    let $list := $map?sequence?*?id
    return
        index-of($list, $pathTo) - 1
};

declare function search:perform($parsedJson as map(*))
as map(*) {
    let $startTiming := util:system-dateTime()
    let $searchExpression := $parsedJson("query")("simple_query_string")("query")
    let $validateQuery := local:validate-query($searchExpression)
    let $returnSize := $parsedJson("size")
    let $returnStart := $parsedJson("from")
    let $options :=
        <options>
            <default-operator>and</default-operator>
            <phrase-slop>3</phrase-slop>
            <leading-wildcard>yes</leading-wildcard>
            <filter-rewrite>no</filter-rewrite>
        </options>

    let $hits :=
        try {
        for $hit in collection($commons:data)//tei:body[ft:query(., $searchExpression, $options)]
            let $score := ft:score($hit)

            let $baseUri := $hit/base-uri()
            let $textgridUri := commons:extract-uri-from-base-uri($baseUri)
            let $edition := commons:get-parent-aggregation($textgridUri)
            let $collection := local:get-language-collection-by-uri($textgridUri)
            let $label := tapi-mani:get-manifest-title($textgridUri)
            let $matches := util:expand($hit)//exist:match
            let $pages := ($matches ! ./preceding::tei:pb[1]/string(@n)) => distinct-values()
            let $type := string($hit/parent::tei:text/@type)
            let $language := string($hit/parent::tei:text/@xml:lang)
            let $pathToBase := $commons:server || "/" || $commons:api-prefix || $collection || "/" || $edition
            let $pathToManifest := $pathToBase || "/manifest.json"
            where $type ne "translation"
            for $page in $pages
            order by $score descending
            let $pathToItem := $pathToBase || "/" || $page || "/latest/item.json"
            let $matchesStrings := $matches[./preceding::tei:pb[1]/string(@n) eq $page] ! string(.)
            let $indexM := local:get-index($collection || "/collection.json", $pathToManifest)
            let $indexI := local:get-index($collection || "/" || $edition || "/manifest.json", $pathToItem)
            return
                map{
                    "type": $type,
                    "lang": $language,
                    "label": $label,
                    "n": $page,
                    "item": $pathToItem,
                    "indices": "m" || $indexM || "_" || "i" || $indexI,
                    "matches": array{
                        for $match in distinct-values($matchesStrings)
                        return
                            map{
                                "match": $match,
                                "occurrencesOnPage": count( $matchesStrings[. eq $match] )
                            }
                        },
                    "further": map{
                        "edition": $edition,
                        "page": $page,
                        "baseUri": $baseUri
                        }
                }
    } catch * {
        util:log-system-out( string-join(($err:code , $err:description, $err:value, $err:module), " || "))
    }

let $count := count($hits)

let $timing := util:system-dateTime()
let $took := ($timing - $startTiming) => seconds-from-duration() (: milliseconds :)

return
    map{
        "request": serialize($parsedJson, map{"method": "json"}),
        "took": $took,
        "hits": map{
            "total": map{
                "value": $count,
                "relation": "eq",
                "manifests": count( distinct-values( $hits ! .("further")("edition") )),
                "matches": sum( ($hits ! .("matches")?*) ! .("occurrencesOnPage") )
            },
            "hits": array{
                $hits
                    [position() le $returnStart + $returnSize]
                    [position() gt $returnStart]
                ! map:remove(., "further")
            }
        }
    }
};
