packages feed

bloodhound-0.21.0.0: src/Database/Bloodhound/Internal/Query.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module Database.Bloodhound.Internal.Query
  ( module X,
    module Database.Bloodhound.Internal.Query,
  )
where

import Bloodhound.Import
import qualified Data.Aeson.KeyMap as X
import qualified Data.HashMap.Strict as HM
import qualified Data.Text as T
import Database.Bloodhound.Common.Script as X
import Database.Bloodhound.Internal.Newtypes
import GHC.Generics

data Query
  = TermQuery Term (Maybe Boost)
  | TermsQuery Key (NonEmpty Text)
  | QueryMatchQuery MatchQuery
  | QueryMultiMatchQuery MultiMatchQuery
  | QueryBoolQuery BoolQuery
  | QueryBoostingQuery BoostingQuery
  | QueryCommonTermsQuery CommonTermsQuery
  | ConstantScoreQuery Query Boost
  | QueryFunctionScoreQuery FunctionScoreQuery
  | QueryDisMaxQuery DisMaxQuery
  | QueryFuzzyLikeThisQuery FuzzyLikeThisQuery
  | QueryFuzzyLikeFieldQuery FuzzyLikeFieldQuery
  | QueryFuzzyQuery FuzzyQuery
  | QueryHasChildQuery HasChildQuery
  | QueryHasParentQuery HasParentQuery
  | IdsQuery [DocId]
  | QueryIndicesQuery IndicesQuery
  | MatchAllQuery (Maybe Boost)
  | QueryMoreLikeThisQuery MoreLikeThisQuery
  | QueryMoreLikeThisFieldQuery MoreLikeThisFieldQuery
  | QueryNestedQuery NestedQuery
  | QueryPrefixQuery PrefixQuery
  | QueryQueryStringQuery QueryStringQuery
  | QuerySimpleQueryStringQuery SimpleQueryStringQuery
  | QueryRangeQuery RangeQuery
  | QueryRegexpQuery RegexpQuery
  | QueryExistsQuery FieldName
  | QueryMatchNoneQuery
  | QueryWildcardQuery WildcardQuery
  deriving (Eq, Show, Generic)

instance ToJSON Query where
  toJSON (TermQuery (Term termQueryField termQueryValue) boost) =
    object
      [ "term"
          .= object [termQueryField .= object merged]
      ]
    where
      base = ["value" .= termQueryValue]
      boosted = maybe [] (return . ("boost" .=)) boost
      merged = mappend base boosted
  toJSON (TermsQuery fieldName terms) =
    object ["terms" .= object conjoined]
    where
      conjoined = [fieldName .= terms]
  toJSON (IdsQuery docIds) =
    object ["ids" .= object conjoined]
    where
      conjoined = ["values" .= fmap toJSON docIds]
  toJSON (QueryQueryStringQuery qQueryStringQuery) =
    object ["query_string" .= qQueryStringQuery]
  toJSON (QueryMatchQuery matchQuery) =
    object ["match" .= matchQuery]
  toJSON (QueryMultiMatchQuery multiMatchQuery) =
    toJSON multiMatchQuery
  toJSON (QueryBoolQuery boolQuery) =
    object ["bool" .= boolQuery]
  toJSON (QueryBoostingQuery boostingQuery) =
    object ["boosting" .= boostingQuery]
  toJSON (QueryCommonTermsQuery commonTermsQuery) =
    object ["common" .= commonTermsQuery]
  toJSON (ConstantScoreQuery query boost) =
    object
      [ "constant_score"
          .= object
            [ "filter" .= query,
              "boost" .= boost
            ]
      ]
  toJSON (QueryFunctionScoreQuery functionScoreQuery) =
    object ["function_score" .= functionScoreQuery]
  toJSON (QueryDisMaxQuery disMaxQuery) =
    object ["dis_max" .= disMaxQuery]
  toJSON (QueryFuzzyLikeThisQuery fuzzyQuery) =
    object ["fuzzy_like_this" .= fuzzyQuery]
  toJSON (QueryFuzzyLikeFieldQuery fuzzyFieldQuery) =
    object ["fuzzy_like_this_field" .= fuzzyFieldQuery]
  toJSON (QueryFuzzyQuery fuzzyQuery) =
    object ["fuzzy" .= fuzzyQuery]
  toJSON (QueryHasChildQuery childQuery) =
    object ["has_child" .= childQuery]
  toJSON (QueryHasParentQuery parentQuery) =
    object ["has_parent" .= parentQuery]
  toJSON (QueryIndicesQuery qIndicesQuery) =
    object ["indices" .= qIndicesQuery]
  toJSON (MatchAllQuery boost) =
    object ["match_all" .= omitNulls ["boost" .= boost]]
  toJSON (QueryMoreLikeThisQuery query) =
    object ["more_like_this" .= query]
  toJSON (QueryMoreLikeThisFieldQuery query) =
    object ["more_like_this_field" .= query]
  toJSON (QueryNestedQuery query) =
    object ["nested" .= query]
  toJSON (QueryPrefixQuery query) =
    object ["prefix" .= query]
  toJSON (QueryRangeQuery query) =
    object ["range" .= query]
  toJSON (QueryRegexpQuery query) =
    object ["regexp" .= query]
  toJSON (QuerySimpleQueryStringQuery query) =
    object ["simple_query_string" .= query]
  toJSON (QueryExistsQuery (FieldName fieldName)) =
    object
      [ "exists"
          .= object
            ["field" .= fieldName]
      ]
  toJSON QueryMatchNoneQuery =
    object ["match_none" .= object []]
  toJSON (QueryWildcardQuery query) =
    object ["wildcard" .= query]

instance FromJSON Query where
  parseJSON v = withObject "Query" parse v
    where
      parse o =
        termQuery
          `taggedWith` "term"
          <|> termsQuery
          `taggedWith` "terms"
          <|> idsQuery
          `taggedWith` "ids"
          <|> queryQueryStringQuery
          `taggedWith` "query_string"
          <|> queryMatchQuery
          `taggedWith` "match"
          <|> queryMultiMatchQuery
          <|> queryBoolQuery
          `taggedWith` "bool"
          <|> queryBoostingQuery
          `taggedWith` "boosting"
          <|> queryCommonTermsQuery
          `taggedWith` "common"
          <|> constantScoreQuery
          `taggedWith` "constant_score"
          <|> queryFunctionScoreQuery
          `taggedWith` "function_score"
          <|> queryDisMaxQuery
          `taggedWith` "dis_max"
          <|> queryFuzzyLikeThisQuery
          `taggedWith` "fuzzy_like_this"
          <|> queryFuzzyLikeFieldQuery
          `taggedWith` "fuzzy_like_this_field"
          <|> queryFuzzyQuery
          `taggedWith` "fuzzy"
          <|> queryHasChildQuery
          `taggedWith` "has_child"
          <|> queryHasParentQuery
          `taggedWith` "has_parent"
          <|> queryIndicesQuery
          `taggedWith` "indices"
          <|> matchAllQuery
          `taggedWith` "match_all"
          <|> queryMoreLikeThisQuery
          `taggedWith` "more_like_this"
          <|> queryMoreLikeThisFieldQuery
          `taggedWith` "more_like_this_field"
          <|> queryNestedQuery
          `taggedWith` "nested"
          <|> queryPrefixQuery
          `taggedWith` "prefix"
          <|> queryRangeQuery
          `taggedWith` "range"
          <|> queryRegexpQuery
          `taggedWith` "regexp"
          <|> querySimpleQueryStringQuery
          `taggedWith` "simple_query_string"
          <|> queryWildcardQuery
          `taggedWith` "wildcard"
        where
          taggedWith parser k = parser =<< o .: k
      termQuery = fieldTagged $ \(FieldName fn) o ->
        TermQuery <$> (Term (fromText fn) <$> o .: "value") <*> o .:? "boost"
      termsQuery o = case HM.toList o of
        [(fn, vs)] -> do
          vals <- parseJSON vs
          case vals of
            x : xs -> return (TermsQuery fn (x :| xs))
            _ -> fail "Expected non empty list of values"
        _ -> fail "Expected object with 1 field-named key"
      idsQuery o = IdsQuery <$> o .: "values"
      queryQueryStringQuery = pure . QueryQueryStringQuery
      queryMatchQuery = pure . QueryMatchQuery
      queryMultiMatchQuery = QueryMultiMatchQuery <$> parseJSON v
      queryBoolQuery = pure . QueryBoolQuery
      queryBoostingQuery = pure . QueryBoostingQuery
      queryCommonTermsQuery = pure . QueryCommonTermsQuery
      constantScoreQuery o = case X.lookup "filter" o of
        Just x ->
          ConstantScoreQuery
            <$> parseJSON x
            <*> o .: "boost"
        _ -> fail "Does not appear to be a ConstantScoreQuery"
      queryFunctionScoreQuery = pure . QueryFunctionScoreQuery
      queryDisMaxQuery = pure . QueryDisMaxQuery
      queryFuzzyLikeThisQuery = pure . QueryFuzzyLikeThisQuery
      queryFuzzyLikeFieldQuery = pure . QueryFuzzyLikeFieldQuery
      queryFuzzyQuery = pure . QueryFuzzyQuery
      queryHasChildQuery = pure . QueryHasChildQuery
      queryHasParentQuery = pure . QueryHasParentQuery
      queryIndicesQuery = pure . QueryIndicesQuery
      matchAllQuery o = MatchAllQuery <$> o .:? "boost"
      queryMoreLikeThisQuery = pure . QueryMoreLikeThisQuery
      queryMoreLikeThisFieldQuery = pure . QueryMoreLikeThisFieldQuery
      queryNestedQuery = pure . QueryNestedQuery
      queryPrefixQuery = pure . QueryPrefixQuery
      queryRangeQuery = pure . QueryRangeQuery
      queryRegexpQuery = pure . QueryRegexpQuery
      querySimpleQueryStringQuery = pure . QuerySimpleQueryStringQuery
      -- queryExistsQuery o = QueryExistsQuery <$> o .: "field"
      queryWildcardQuery = pure . QueryWildcardQuery

-- | As of Elastic 2.0, 'Filters' are just 'Queries' housed in a
--   Bool Query, and flagged in a different context.
newtype Filter = Filter {unFilter :: Query}
  deriving (Eq, Show)

instance ToJSON Filter where
  toJSON = toJSON . unFilter

instance FromJSON Filter where
  parseJSON v = Filter <$> parseJSON v

data RegexpQuery = RegexpQuery
  { regexpQueryField :: FieldName,
    regexpQuery :: Regexp,
    regexpQueryFlags :: RegexpFlags,
    regexpQueryBoost :: Maybe Boost
  }
  deriving (Eq, Show, Generic)

instance ToJSON RegexpQuery where
  toJSON
    ( RegexpQuery
        (FieldName rqQueryField)
        (Regexp regexpQueryQuery)
        rqQueryFlags
        rqQueryBoost
      ) =
      object [fromText rqQueryField .= omitNulls base]
      where
        base =
          [ "value" .= regexpQueryQuery,
            "flags" .= rqQueryFlags,
            "boost" .= rqQueryBoost
          ]

instance FromJSON RegexpQuery where
  parseJSON = withObject "RegexpQuery" parse
    where
      parse = fieldTagged $ \fn o ->
        RegexpQuery fn
          <$> o .: "value"
          <*> o .: "flags"
          <*> o .:? "boost"

data WildcardQuery = WildcardQuery
  { wildcardQueryField :: FieldName,
    wildcardQuery :: Key,
    wildcardQueryBoost :: Maybe Boost
  }
  deriving (Eq, Show, Generic)

instance ToJSON WildcardQuery where
  toJSON
    ( WildcardQuery
        (FieldName wcQueryField)
        (wcQueryQuery)
        wcQueryBoost
      ) =
      object [fromText wcQueryField .= omitNulls base]
      where
        base =
          [ "value" .= wcQueryQuery,
            "boost" .= wcQueryBoost
          ]

instance FromJSON WildcardQuery where
  parseJSON = withObject "WildcardQuery" parse
    where
      parse = fieldTagged $ \fn o ->
        WildcardQuery fn
          <$> o .: "value"
          <*> o .:? "boost"

data RangeQuery = RangeQuery
  { rangeQueryField :: FieldName,
    rangeQueryRange :: RangeValue,
    rangeQueryBoost :: Boost
  }
  deriving (Eq, Show, Generic)

instance ToJSON RangeQuery where
  toJSON (RangeQuery (FieldName fieldName) range boost) =
    object [fromText fieldName .= object conjoined]
    where
      conjoined = ("boost" .= boost) : rangeValueToPair range

instance FromJSON RangeQuery where
  parseJSON = withObject "RangeQuery" parse
    where
      parse = fieldTagged $ \fn o ->
        RangeQuery fn
          <$> parseJSON (Object o)
          <*> o .: "boost"

mkRangeQuery :: FieldName -> RangeValue -> RangeQuery
mkRangeQuery f r = RangeQuery f r (Boost 1.0)

data SimpleQueryStringQuery = SimpleQueryStringQuery
  { simpleQueryStringQuery :: QueryString,
    simpleQueryStringField :: Maybe FieldOrFields,
    simpleQueryStringOperator :: Maybe BooleanOperator,
    simpleQueryStringAnalyzer :: Maybe Analyzer,
    simpleQueryStringFlags :: Maybe (NonEmpty SimpleQueryFlag),
    simpleQueryStringLowercaseExpanded :: Maybe LowercaseExpanded,
    simpleQueryStringLocale :: Maybe Locale
  }
  deriving (Eq, Show, Generic)

instance ToJSON SimpleQueryStringQuery where
  toJSON SimpleQueryStringQuery {..} =
    omitNulls (base ++ maybeAdd)
    where
      base = ["query" .= simpleQueryStringQuery]
      maybeAdd =
        [ "fields" .= simpleQueryStringField,
          "default_operator" .= simpleQueryStringOperator,
          "analyzer" .= simpleQueryStringAnalyzer,
          "flags" .= simpleQueryStringFlags,
          "lowercase_expanded_terms" .= simpleQueryStringLowercaseExpanded,
          "locale" .= simpleQueryStringLocale
        ]

instance FromJSON SimpleQueryStringQuery where
  parseJSON = withObject "SimpleQueryStringQuery" parse
    where
      parse o =
        SimpleQueryStringQuery
          <$> o .: "query"
          <*> o .:? "fields"
          <*> o .:? "default_operator"
          <*> o .:? "analyzer"
          <*> (parseFlags <$> o .:? "flags")
          <*> o .:? "lowercase_expanded_terms"
          <*> o .:? "locale"
      parseFlags (Just (x : xs)) = Just (x :| xs)
      parseFlags _ = Nothing

data SimpleQueryFlag
  = SimpleQueryAll
  | SimpleQueryNone
  | SimpleQueryAnd
  | SimpleQueryOr
  | SimpleQueryPrefix
  | SimpleQueryPhrase
  | SimpleQueryPrecedence
  | SimpleQueryEscape
  | SimpleQueryWhitespace
  | SimpleQueryFuzzy
  | SimpleQueryNear
  | SimpleQuerySlop
  deriving (Eq, Show, Generic)

instance ToJSON SimpleQueryFlag where
  toJSON SimpleQueryAll = "ALL"
  toJSON SimpleQueryNone = "NONE"
  toJSON SimpleQueryAnd = "AND"
  toJSON SimpleQueryOr = "OR"
  toJSON SimpleQueryPrefix = "PREFIX"
  toJSON SimpleQueryPhrase = "PHRASE"
  toJSON SimpleQueryPrecedence = "PRECEDENCE"
  toJSON SimpleQueryEscape = "ESCAPE"
  toJSON SimpleQueryWhitespace = "WHITESPACE"
  toJSON SimpleQueryFuzzy = "FUZZY"
  toJSON SimpleQueryNear = "NEAR"
  toJSON SimpleQuerySlop = "SLOP"

instance FromJSON SimpleQueryFlag where
  parseJSON = withText "SimpleQueryFlag" parse
    where
      parse "ALL" = pure SimpleQueryAll
      parse "NONE" = pure SimpleQueryNone
      parse "AND" = pure SimpleQueryAnd
      parse "OR" = pure SimpleQueryOr
      parse "PREFIX" = pure SimpleQueryPrefix
      parse "PHRASE" = pure SimpleQueryPhrase
      parse "PRECEDENCE" = pure SimpleQueryPrecedence
      parse "ESCAPE" = pure SimpleQueryEscape
      parse "WHITESPACE" = pure SimpleQueryWhitespace
      parse "FUZZY" = pure SimpleQueryFuzzy
      parse "NEAR" = pure SimpleQueryNear
      parse "SLOP" = pure SimpleQuerySlop
      parse f = fail ("Unexpected SimpleQueryFlag: " <> show f)

-- use_dis_max and tie_breaker when fields are plural?
data QueryStringQuery = QueryStringQuery
  { queryStringQuery :: QueryString,
    queryStringDefaultField :: Maybe FieldName,
    queryStringOperator :: Maybe BooleanOperator,
    queryStringAnalyzer :: Maybe Analyzer,
    queryStringAllowLeadingWildcard :: Maybe AllowLeadingWildcard,
    queryStringLowercaseExpanded :: Maybe LowercaseExpanded,
    queryStringEnablePositionIncrements :: Maybe EnablePositionIncrements,
    queryStringFuzzyMaxExpansions :: Maybe MaxExpansions,
    queryStringFuzziness :: Maybe Fuzziness,
    queryStringFuzzyPrefixLength :: Maybe PrefixLength,
    queryStringPhraseSlop :: Maybe PhraseSlop,
    queryStringBoost :: Maybe Boost,
    queryStringAnalyzeWildcard :: Maybe AnalyzeWildcard,
    queryStringGeneratePhraseQueries :: Maybe GeneratePhraseQueries,
    queryStringMinimumShouldMatch :: Maybe MinimumMatch,
    queryStringLenient :: Maybe Lenient,
    queryStringLocale :: Maybe Locale
  }
  deriving (Eq, Show, Generic)

instance ToJSON QueryStringQuery where
  toJSON
    ( QueryStringQuery
        qsQueryString
        qsDefaultField
        qsOperator
        qsAnalyzer
        qsAllowWildcard
        qsLowercaseExpanded
        qsEnablePositionIncrements
        qsFuzzyMaxExpansions
        qsFuzziness
        qsFuzzyPrefixLength
        qsPhraseSlop
        qsBoost
        qsAnalyzeWildcard
        qsGeneratePhraseQueries
        qsMinimumShouldMatch
        qsLenient
        qsLocale
      ) =
      omitNulls base
      where
        base =
          [ "query" .= qsQueryString,
            "default_field" .= qsDefaultField,
            "default_operator" .= qsOperator,
            "analyzer" .= qsAnalyzer,
            "allow_leading_wildcard" .= qsAllowWildcard,
            "lowercase_expanded_terms" .= qsLowercaseExpanded,
            "enable_position_increments" .= qsEnablePositionIncrements,
            "fuzzy_max_expansions" .= qsFuzzyMaxExpansions,
            "fuzziness" .= qsFuzziness,
            "fuzzy_prefix_length" .= qsFuzzyPrefixLength,
            "phrase_slop" .= qsPhraseSlop,
            "boost" .= qsBoost,
            "analyze_wildcard" .= qsAnalyzeWildcard,
            "auto_generate_phrase_queries" .= qsGeneratePhraseQueries,
            "minimum_should_match" .= qsMinimumShouldMatch,
            "lenient" .= qsLenient,
            "locale" .= qsLocale
          ]

instance FromJSON QueryStringQuery where
  parseJSON = withObject "QueryStringQuery" parse
    where
      parse o =
        QueryStringQuery
          <$> o .: "query"
          <*> o .:? "default_field"
          <*> o .:? "default_operator"
          <*> o .:? "analyzer"
          <*> o .:? "allow_leading_wildcard"
          <*> o .:? "lowercase_expanded_terms"
          <*> o .:? "enable_position_increments"
          <*> o .:? "fuzzy_max_expansions"
          <*> o .:? "fuzziness"
          <*> o .:? "fuzzy_prefix_length"
          <*> o .:? "phrase_slop"
          <*> o .:? "boost"
          <*> o .:? "analyze_wildcard"
          <*> o .:? "auto_generate_phrase_queries"
          <*> o .:? "minimum_should_match"
          <*> o .:? "lenient"
          <*> o .:? "locale"

mkQueryStringQuery :: QueryString -> QueryStringQuery
mkQueryStringQuery qs =
  QueryStringQuery
    qs
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing

data FieldOrFields
  = FofField FieldName
  | FofFields (NonEmpty FieldName)
  deriving (Eq, Show, Generic)

instance ToJSON FieldOrFields where
  toJSON (FofField fieldName) =
    toJSON fieldName
  toJSON (FofFields fieldNames) =
    toJSON fieldNames

instance FromJSON FieldOrFields where
  parseJSON v =
    FofField <$> parseJSON v
      <|> FofFields <$> (parseNEJSON =<< parseJSON v)

data PrefixQuery = PrefixQuery
  { prefixQueryField :: FieldName,
    prefixQueryPrefixValue :: Text,
    prefixQueryBoost :: Maybe Boost
  }
  deriving (Eq, Show, Generic)

instance ToJSON PrefixQuery where
  toJSON (PrefixQuery (FieldName fieldName) queryValue boost) =
    object [fromText fieldName .= omitNulls base]
    where
      base =
        [ "value" .= queryValue,
          "boost" .= boost
        ]

instance FromJSON PrefixQuery where
  parseJSON = withObject "PrefixQuery" parse
    where
      parse = fieldTagged $ \fn o ->
        PrefixQuery fn
          <$> o .: "value"
          <*> o .:? "boost"

data NestedQuery = NestedQuery
  { nestedQueryPath :: QueryPath,
    nestedQueryScoreType :: ScoreType,
    nestedQuery :: Query,
    nestedQueryInnerHits :: Maybe InnerHits
  }
  deriving (Eq, Show, Generic)

instance ToJSON NestedQuery where
  toJSON (NestedQuery nqPath nqScoreType nqQuery nqInnerHits) =
    omitNulls
      [ "path" .= nqPath,
        "score_mode" .= nqScoreType,
        "query" .= nqQuery,
        "inner_hits" .= nqInnerHits
      ]

instance FromJSON NestedQuery where
  parseJSON = withObject "NestedQuery" parse
    where
      parse o =
        NestedQuery
          <$> o .: "path"
          <*> o .: "score_mode"
          <*> o .: "query"
          <*> o .:? "inner_hits"

data MoreLikeThisFieldQuery = MoreLikeThisFieldQuery
  { moreLikeThisFieldText :: Text,
    moreLikeThisFieldFields :: FieldName,
    -- default 0.3 (30%)
    moreLikeThisFieldPercentMatch :: Maybe PercentMatch,
    moreLikeThisFieldMinimumTermFreq :: Maybe MinimumTermFrequency,
    moreLikeThisFieldMaxQueryTerms :: Maybe MaxQueryTerms,
    moreLikeThisFieldStopWords :: Maybe (NonEmpty StopWord),
    moreLikeThisFieldMinDocFrequency :: Maybe MinDocFrequency,
    moreLikeThisFieldMaxDocFrequency :: Maybe MaxDocFrequency,
    moreLikeThisFieldMinWordLength :: Maybe MinWordLength,
    moreLikeThisFieldMaxWordLength :: Maybe MaxWordLength,
    moreLikeThisFieldBoostTerms :: Maybe BoostTerms,
    moreLikeThisFieldBoost :: Maybe Boost,
    moreLikeThisFieldAnalyzer :: Maybe Analyzer
  }
  deriving (Eq, Show, Generic)

instance ToJSON MoreLikeThisFieldQuery where
  toJSON
    ( MoreLikeThisFieldQuery
        text
        (FieldName fieldName)
        percent
        mtf
        mqt
        stopwords
        mindf
        maxdf
        minwl
        maxwl
        boostTerms
        boost
        analyzer
      ) =
      object [fromText fieldName .= omitNulls base]
      where
        base =
          [ "like_text" .= text,
            "percent_terms_to_match" .= percent,
            "min_term_freq" .= mtf,
            "max_query_terms" .= mqt,
            "stop_words" .= stopwords,
            "min_doc_freq" .= mindf,
            "max_doc_freq" .= maxdf,
            "min_word_length" .= minwl,
            "max_word_length" .= maxwl,
            "boost_terms" .= boostTerms,
            "boost" .= boost,
            "analyzer" .= analyzer
          ]

instance FromJSON MoreLikeThisFieldQuery where
  parseJSON = withObject "MoreLikeThisFieldQuery" parse
    where
      parse = fieldTagged $ \fn o ->
        MoreLikeThisFieldQuery
          <$> o .: "like_text"
          <*> pure fn
          <*> o .:? "percent_terms_to_match"
          <*> o .:? "min_term_freq"
          <*> o .:? "max_query_terms"
          -- <*> (optionalNE =<< o .:? "stop_words")
          <*> o .:? "stop_words"
          <*> o .:? "min_doc_freq"
          <*> o .:? "max_doc_freq"
          <*> o .:? "min_word_length"
          <*> o .:? "max_word_length"
          <*> o .:? "boost_terms"
          <*> o .:? "boost"
          <*> o .:? "analyzer"

-- optionalNE = maybe (pure Nothing) (fmap Just . parseNEJSON)

data MoreLikeThisQuery = MoreLikeThisQuery
  { moreLikeThisText :: Text,
    moreLikeThisFields :: Maybe (NonEmpty FieldName),
    -- default 0.3 (30%)
    moreLikeThisPercentMatch :: Maybe PercentMatch,
    moreLikeThisMinimumTermFreq :: Maybe MinimumTermFrequency,
    moreLikeThisMaxQueryTerms :: Maybe MaxQueryTerms,
    moreLikeThisStopWords :: Maybe (NonEmpty StopWord),
    moreLikeThisMinDocFrequency :: Maybe MinDocFrequency,
    moreLikeThisMaxDocFrequency :: Maybe MaxDocFrequency,
    moreLikeThisMinWordLength :: Maybe MinWordLength,
    moreLikeThisMaxWordLength :: Maybe MaxWordLength,
    moreLikeThisBoostTerms :: Maybe BoostTerms,
    moreLikeThisBoost :: Maybe Boost,
    moreLikeThisAnalyzer :: Maybe Analyzer
  }
  deriving (Eq, Show, Generic)

instance ToJSON MoreLikeThisQuery where
  toJSON
    ( MoreLikeThisQuery
        text
        fields
        percent
        mtf
        mqt
        stopwords
        mindf
        maxdf
        minwl
        maxwl
        boostTerms
        boost
        analyzer
      ) =
      omitNulls base
      where
        base =
          [ "like_text" .= text,
            "fields" .= fields,
            "percent_terms_to_match" .= percent,
            "min_term_freq" .= mtf,
            "max_query_terms" .= mqt,
            "stop_words" .= stopwords,
            "min_doc_freq" .= mindf,
            "max_doc_freq" .= maxdf,
            "min_word_length" .= minwl,
            "max_word_length" .= maxwl,
            "boost_terms" .= boostTerms,
            "boost" .= boost,
            "analyzer" .= analyzer
          ]

instance FromJSON MoreLikeThisQuery where
  parseJSON = withObject "MoreLikeThisQuery" parse
    where
      parse o =
        MoreLikeThisQuery
          <$> o .: "like_text"
          -- <*> (optionalNE =<< o .:? "fields")
          <*> o .:? "fields"
          <*> o .:? "percent_terms_to_match"
          <*> o .:? "min_term_freq"
          <*> o .:? "max_query_terms"
          -- <*> (optionalNE =<< o .:? "stop_words")
          <*> o .:? "stop_words"
          <*> o .:? "min_doc_freq"
          <*> o .:? "max_doc_freq"
          <*> o .:? "min_word_length"
          <*> o .:? "max_word_length"
          <*> o .:? "boost_terms"
          <*> o .:? "boost"
          <*> o .:? "analyzer"

-- optionalNE = maybe (pure Nothing) (fmap Just . parseNEJSON)

data IndicesQuery = IndicesQuery
  { indicesQueryIndices :: [IndexName],
    indicesQuery :: Query,
    -- default "all"
    indicesQueryNoMatch :: Maybe Query
  }
  deriving (Eq, Show, Generic)

instance ToJSON IndicesQuery where
  toJSON (IndicesQuery indices query noMatch) =
    omitNulls
      [ "indices" .= indices,
        "no_match_query" .= noMatch,
        "query" .= query
      ]

instance FromJSON IndicesQuery where
  parseJSON = withObject "IndicesQuery" parse
    where
      parse o =
        IndicesQuery
          <$> o .:? "indices" .!= []
          <*> o .: "query"
          <*> o .:? "no_match_query"

data HasParentQuery = HasParentQuery
  { hasParentQueryType :: RelationName,
    hasParentQuery :: Query,
    hasParentQueryScore :: Maybe AggregateParentScore,
    hasParentIgnoreUnmapped :: Maybe IgnoreUnmapped
  }
  deriving (Eq, Show, Generic)

instance ToJSON HasParentQuery where
  toJSON (HasParentQuery queryType query scoreType ignoreUnmapped) =
    omitNulls
      [ "parent_type" .= queryType,
        "score" .= scoreType,
        "query" .= query,
        "ignore_unmapped" .= ignoreUnmapped
      ]

instance FromJSON HasParentQuery where
  parseJSON = withObject "HasParentQuery" parse
    where
      parse o =
        HasParentQuery
          <$> o .: "parent_type"
          <*> o .: "query"
          <*> o .:? "score"
          <*> o .:? "ignore_unmapped"

data HasChildQuery = HasChildQuery
  { hasChildQueryType :: RelationName,
    hasChildQuery :: Query,
    hasChildQueryScoreType :: Maybe ScoreType,
    hasChildIgnoreUnmappped :: Maybe IgnoreUnmapped,
    hasChildMinChildren :: Maybe MinChildren,
    hasChildMaxChildren :: Maybe MaxChildren
  }
  deriving (Eq, Show, Generic)

instance ToJSON HasChildQuery where
  toJSON (HasChildQuery queryType query scoreType ignoreUnmapped minChildren maxChildren) =
    omitNulls
      [ "query" .= query,
        "score_mode" .= scoreType,
        "type" .= queryType,
        "min_children" .= minChildren,
        "max_children" .= maxChildren,
        "ignore_unmapped" .= ignoreUnmapped
      ]

instance FromJSON HasChildQuery where
  parseJSON = withObject "HasChildQuery" parse
    where
      parse o =
        HasChildQuery
          <$> o .: "type"
          <*> o .: "query"
          <*> o .:? "score_mode"
          <*> o .:? "ignore_unmapped"
          <*> o .:? "min_children"
          <*> o .:? "max_children"

data ScoreType
  = ScoreTypeMax
  | ScoreTypeSum
  | ScoreTypeAvg
  | ScoreTypeNone
  deriving (Eq, Show, Generic)

instance ToJSON ScoreType where
  toJSON ScoreTypeMax = "max"
  toJSON ScoreTypeAvg = "avg"
  toJSON ScoreTypeSum = "sum"
  toJSON ScoreTypeNone = "none"

instance FromJSON ScoreType where
  parseJSON = withText "ScoreType" parse
    where
      parse "max" = pure ScoreTypeMax
      parse "avg" = pure ScoreTypeAvg
      parse "sum" = pure ScoreTypeSum
      parse "none" = pure ScoreTypeNone
      parse t = fail ("Unexpected ScoreType: " <> show t)

data FuzzyQuery = FuzzyQuery
  { fuzzyQueryField :: FieldName,
    fuzzyQueryValue :: Text,
    fuzzyQueryPrefixLength :: PrefixLength,
    fuzzyQueryMaxExpansions :: MaxExpansions,
    fuzzyQueryFuzziness :: Fuzziness,
    fuzzyQueryBoost :: Maybe Boost
  }
  deriving (Eq, Show, Generic)

instance ToJSON FuzzyQuery where
  toJSON
    ( FuzzyQuery
        (FieldName fieldName)
        queryText
        prefixLength
        maxEx
        fuzziness
        boost
      ) =
      object [fromText fieldName .= omitNulls base]
      where
        base =
          [ "value" .= queryText,
            "fuzziness" .= fuzziness,
            "prefix_length" .= prefixLength,
            "boost" .= boost,
            "max_expansions" .= maxEx
          ]

instance FromJSON FuzzyQuery where
  parseJSON = withObject "FuzzyQuery" parse
    where
      parse = fieldTagged $ \fn o ->
        FuzzyQuery fn
          <$> o .: "value"
          <*> o .: "prefix_length"
          <*> o .: "max_expansions"
          <*> o .: "fuzziness"
          <*> o .:? "boost"

data FuzzyLikeFieldQuery = FuzzyLikeFieldQuery
  { fuzzyLikeField :: FieldName,
    -- anaphora is good for the soul.
    fuzzyLikeFieldText :: Text,
    fuzzyLikeFieldMaxQueryTerms :: MaxQueryTerms,
    fuzzyLikeFieldIgnoreTermFrequency :: IgnoreTermFrequency,
    fuzzyLikeFieldFuzziness :: Fuzziness,
    fuzzyLikeFieldPrefixLength :: PrefixLength,
    fuzzyLikeFieldBoost :: Boost,
    fuzzyLikeFieldAnalyzer :: Maybe Analyzer
  }
  deriving (Eq, Show, Generic)

instance ToJSON FuzzyLikeFieldQuery where
  toJSON
    ( FuzzyLikeFieldQuery
        (FieldName fieldName)
        fieldText
        maxTerms
        ignoreFreq
        fuzziness
        prefixLength
        boost
        analyzer
      ) =
      object
        [ fromText fieldName
            .= omitNulls
              [ "like_text" .= fieldText,
                "max_query_terms" .= maxTerms,
                "ignore_tf" .= ignoreFreq,
                "fuzziness" .= fuzziness,
                "prefix_length" .= prefixLength,
                "analyzer" .= analyzer,
                "boost" .= boost
              ]
        ]

instance FromJSON FuzzyLikeFieldQuery where
  parseJSON = withObject "FuzzyLikeFieldQuery" parse
    where
      parse = fieldTagged $ \fn o ->
        FuzzyLikeFieldQuery fn
          <$> o .: "like_text"
          <*> o .: "max_query_terms"
          <*> o .: "ignore_tf"
          <*> o .: "fuzziness"
          <*> o .: "prefix_length"
          <*> o .: "boost"
          <*> o .:? "analyzer"

data FuzzyLikeThisQuery = FuzzyLikeThisQuery
  { fuzzyLikeFields :: [FieldName],
    fuzzyLikeText :: Text,
    fuzzyLikeMaxQueryTerms :: MaxQueryTerms,
    fuzzyLikeIgnoreTermFrequency :: IgnoreTermFrequency,
    fuzzyLikeFuzziness :: Fuzziness,
    fuzzyLikePrefixLength :: PrefixLength,
    fuzzyLikeBoost :: Boost,
    fuzzyLikeAnalyzer :: Maybe Analyzer
  }
  deriving (Eq, Show, Generic)

instance ToJSON FuzzyLikeThisQuery where
  toJSON
    ( FuzzyLikeThisQuery
        fields
        text
        maxTerms
        ignoreFreq
        fuzziness
        prefixLength
        boost
        analyzer
      ) =
      omitNulls base
      where
        base =
          [ "fields" .= fields,
            "like_text" .= text,
            "max_query_terms" .= maxTerms,
            "ignore_tf" .= ignoreFreq,
            "fuzziness" .= fuzziness,
            "prefix_length" .= prefixLength,
            "analyzer" .= analyzer,
            "boost" .= boost
          ]

instance FromJSON FuzzyLikeThisQuery where
  parseJSON = withObject "FuzzyLikeThisQuery" parse
    where
      parse o =
        FuzzyLikeThisQuery
          <$> o .:? "fields" .!= []
          <*> o .: "like_text"
          <*> o .: "max_query_terms"
          <*> o .: "ignore_tf"
          <*> o .: "fuzziness"
          <*> o .: "prefix_length"
          <*> o .: "boost"
          <*> o .:? "analyzer"

data DisMaxQuery = DisMaxQuery
  { disMaxQueries :: [Query],
    -- default 0.0
    disMaxTiebreaker :: Tiebreaker,
    disMaxBoost :: Maybe Boost
  }
  deriving (Eq, Show, Generic)

instance ToJSON DisMaxQuery where
  toJSON (DisMaxQuery queries tiebreaker boost) =
    omitNulls base
    where
      base =
        [ "queries" .= queries,
          "boost" .= boost,
          "tie_breaker" .= tiebreaker
        ]

instance FromJSON DisMaxQuery where
  parseJSON = withObject "DisMaxQuery" parse
    where
      parse o =
        DisMaxQuery
          <$> o .:? "queries" .!= []
          <*> o .: "tie_breaker"
          <*> o .:? "boost"

data MatchQuery = MatchQuery
  { matchQueryField :: FieldName,
    matchQueryQueryString :: QueryString,
    matchQueryOperator :: BooleanOperator,
    matchQueryZeroTerms :: ZeroTermsQuery,
    matchQueryCutoffFrequency :: Maybe CutoffFrequency,
    matchQueryMatchType :: Maybe MatchQueryType,
    matchQueryAnalyzer :: Maybe Analyzer,
    matchQueryMaxExpansions :: Maybe MaxExpansions,
    matchQueryLenient :: Maybe Lenient,
    matchQueryBoost :: Maybe Boost,
    matchQueryMinimumShouldMatch :: Maybe Text,
    matchQueryFuzziness :: Maybe Fuzziness
  }
  deriving (Eq, Show, Generic)

instance ToJSON MatchQuery where
  toJSON
    ( MatchQuery
        (FieldName fieldName)
        (QueryString mqQueryString)
        booleanOperator
        zeroTermsQuery
        cutoffFrequency
        matchQueryType
        analyzer
        maxExpansions
        lenient
        boost
        minShouldMatch
        mqFuzziness
      ) =
      object [fromText fieldName .= omitNulls base]
      where
        base =
          [ "query" .= mqQueryString,
            "operator" .= booleanOperator,
            "zero_terms_query" .= zeroTermsQuery,
            "cutoff_frequency" .= cutoffFrequency,
            "type" .= matchQueryType,
            "analyzer" .= analyzer,
            "max_expansions" .= maxExpansions,
            "lenient" .= lenient,
            "boost" .= boost,
            "minimum_should_match" .= minShouldMatch,
            "fuzziness" .= mqFuzziness
          ]

instance FromJSON MatchQuery where
  parseJSON = withObject "MatchQuery" parse
    where
      parse = fieldTagged $ \fn o ->
        MatchQuery fn
          <$> o .: "query"
          <*> o .: "operator"
          <*> o .: "zero_terms_query"
          <*> o .:? "cutoff_frequency"
          <*> o .:? "type"
          <*> o .:? "analyzer"
          <*> o .:? "max_expansions"
          <*> o .:? "lenient"
          <*> o .:? "boost"
          <*> o .:? "minimum_should_match"
          <*> o .:? "fuzziness"

-- | 'mkMatchQuery' is a convenience function that defaults the less common parameters,
--    enabling you to provide only the 'FieldName' and 'QueryString' to make a 'MatchQuery'
mkMatchQuery :: FieldName -> QueryString -> MatchQuery
mkMatchQuery field query = MatchQuery field query Or ZeroTermsNone Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing

data MatchQueryType
  = MatchPhrase
  | MatchPhrasePrefix
  deriving (Eq, Show, Generic)

instance ToJSON MatchQueryType where
  toJSON MatchPhrase = "phrase"
  toJSON MatchPhrasePrefix = "phrase_prefix"

instance FromJSON MatchQueryType where
  parseJSON = withText "MatchQueryType" parse
    where
      parse "phrase" = pure MatchPhrase
      parse "phrase_prefix" = pure MatchPhrasePrefix
      parse t = fail ("Unexpected MatchQueryType: " <> show t)

data MultiMatchQuery = MultiMatchQuery
  { multiMatchQueryFields :: [FieldName],
    multiMatchQueryString :: QueryString,
    multiMatchQueryOperator :: BooleanOperator,
    multiMatchQueryZeroTerms :: ZeroTermsQuery,
    multiMatchQueryTiebreaker :: Maybe Tiebreaker,
    multiMatchQueryType :: Maybe MultiMatchQueryType,
    multiMatchQueryCutoffFrequency :: Maybe CutoffFrequency,
    multiMatchQueryAnalyzer :: Maybe Analyzer,
    multiMatchQueryMaxExpansions :: Maybe MaxExpansions,
    multiMatchQueryLenient :: Maybe Lenient
  }
  deriving (Eq, Show, Generic)

instance ToJSON MultiMatchQuery where
  toJSON
    ( MultiMatchQuery
        fields
        (QueryString query)
        boolOp
        ztQ
        tb
        mmqt
        cf
        analyzer
        maxEx
        lenient
      ) =
      object ["multi_match" .= omitNulls base]
      where
        base =
          [ "fields" .= fmap toJSON fields,
            "query" .= query,
            "operator" .= boolOp,
            "zero_terms_query" .= ztQ,
            "tie_breaker" .= tb,
            "type" .= mmqt,
            "cutoff_frequency" .= cf,
            "analyzer" .= analyzer,
            "max_expansions" .= maxEx,
            "lenient" .= lenient
          ]

instance FromJSON MultiMatchQuery where
  parseJSON = withObject "MultiMatchQuery" parse
    where
      parse raw = do
        o <- raw .: "multi_match"
        MultiMatchQuery
          <$> o .:? "fields" .!= []
          <*> o .: "query"
          <*> o .: "operator"
          <*> o .: "zero_terms_query"
          <*> o .:? "tie_breaker"
          <*> o .:? "type"
          <*> o .:? "cutoff_frequency"
          <*> o .:? "analyzer"
          <*> o .:? "max_expansions"
          <*> o .:? "lenient"

-- | 'mkMultiMatchQuery' is a convenience function that defaults the less common parameters,
--    enabling you to provide only the list of 'FieldName's and 'QueryString' to
--    make a 'MultiMatchQuery'.
mkMultiMatchQuery :: [FieldName] -> QueryString -> MultiMatchQuery
mkMultiMatchQuery matchFields query =
  MultiMatchQuery
    matchFields
    query
    Or
    ZeroTermsNone
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing

data MultiMatchQueryType
  = MultiMatchBestFields
  | MultiMatchMostFields
  | MultiMatchCrossFields
  | MultiMatchPhrase
  | MultiMatchPhrasePrefix
  deriving (Eq, Show, Generic)

instance ToJSON MultiMatchQueryType where
  toJSON MultiMatchBestFields = "best_fields"
  toJSON MultiMatchMostFields = "most_fields"
  toJSON MultiMatchCrossFields = "cross_fields"
  toJSON MultiMatchPhrase = "phrase"
  toJSON MultiMatchPhrasePrefix = "phrase_prefix"

instance FromJSON MultiMatchQueryType where
  parseJSON = withText "MultiMatchPhrasePrefix" parse
    where
      parse "best_fields" = pure MultiMatchBestFields
      parse "most_fields" = pure MultiMatchMostFields
      parse "cross_fields" = pure MultiMatchCrossFields
      parse "phrase" = pure MultiMatchPhrase
      parse "phrase_prefix" = pure MultiMatchPhrasePrefix
      parse t = fail ("Unexpected MultiMatchPhrasePrefix: " <> show t)

data BoolQuery = BoolQuery
  { boolQueryMustMatch :: [Query],
    boolQueryFilter :: [Filter],
    boolQueryMustNotMatch :: [Query],
    boolQueryShouldMatch :: [Query],
    boolQueryMinimumShouldMatch :: Maybe MinimumMatch,
    boolQueryBoost :: Maybe Boost,
    boolQueryDisableCoord :: Maybe DisableCoord
  }
  deriving (Eq, Show, Generic)

instance ToJSON BoolQuery where
  toJSON (BoolQuery mustM filterM' notM shouldM bqMin boost disableCoord) =
    omitNulls base
    where
      base =
        [ "must" .= mustM,
          "filter" .= filterM',
          "must_not" .= notM,
          "should" .= shouldM,
          "minimum_should_match" .= bqMin,
          "boost" .= boost,
          "disable_coord" .= disableCoord
        ]

instance FromJSON BoolQuery where
  parseJSON = withObject "BoolQuery" parse
    where
      parse o =
        BoolQuery
          <$> o .:? "must" .!= []
          <*> o .:? "filter" .!= []
          <*> o .:? "must_not" .!= []
          <*> o .:? "should" .!= []
          <*> o .:? "minimum_should_match"
          <*> o .:? "boost"
          <*> o .:? "disable_coord"

mkBoolQuery :: [Query] -> [Filter] -> [Query] -> [Query] -> BoolQuery
mkBoolQuery must filt mustNot should =
  BoolQuery must filt mustNot should Nothing Nothing Nothing

data BoostingQuery = BoostingQuery
  { positiveQuery :: Query,
    negativeQuery :: Query,
    negativeBoost :: Boost
  }
  deriving (Eq, Show, Generic)

instance ToJSON BoostingQuery where
  toJSON (BoostingQuery bqPositiveQuery bqNegativeQuery bqNegativeBoost) =
    object
      [ "positive" .= bqPositiveQuery,
        "negative" .= bqNegativeQuery,
        "negative_boost" .= bqNegativeBoost
      ]

instance FromJSON BoostingQuery where
  parseJSON = withObject "BoostingQuery" parse
    where
      parse o =
        BoostingQuery
          <$> o .: "positive"
          <*> o .: "negative"
          <*> o .: "negative_boost"

data CommonTermsQuery = CommonTermsQuery
  { commonField :: FieldName,
    commonQuery :: QueryString,
    commonCutoffFrequency :: CutoffFrequency,
    commonLowFreqOperator :: BooleanOperator,
    commonHighFreqOperator :: BooleanOperator,
    commonMinimumShouldMatch :: Maybe CommonMinimumMatch,
    commonBoost :: Maybe Boost,
    commonAnalyzer :: Maybe Analyzer,
    commonDisableCoord :: Maybe DisableCoord
  }
  deriving (Eq, Show, Generic)

instance ToJSON CommonTermsQuery where
  toJSON
    ( CommonTermsQuery
        (FieldName fieldName)
        (QueryString query)
        cf
        lfo
        hfo
        msm
        boost
        analyzer
        disableCoord
      ) =
      object [fromText fieldName .= omitNulls base]
      where
        base =
          [ "query" .= query,
            "cutoff_frequency" .= cf,
            "low_freq_operator" .= lfo,
            "minimum_should_match" .= msm,
            "boost" .= boost,
            "analyzer" .= analyzer,
            "disable_coord" .= disableCoord,
            "high_freq_operator" .= hfo
          ]

instance FromJSON CommonTermsQuery where
  parseJSON = withObject "CommonTermsQuery" parse
    where
      parse = fieldTagged $ \fn o ->
        CommonTermsQuery fn
          <$> o .: "query"
          <*> o .: "cutoff_frequency"
          <*> o .: "low_freq_operator"
          <*> o .: "high_freq_operator"
          <*> o .:? "minimum_should_match"
          <*> o .:? "boost"
          <*> o .:? "analyzer"
          <*> o .:? "disable_coord"

data CommonMinimumMatch
  = CommonMinimumMatchHighLow MinimumMatchHighLow
  | CommonMinimumMatch MinimumMatch
  deriving (Eq, Show, Generic)

instance ToJSON CommonMinimumMatch where
  toJSON (CommonMinimumMatch mm) = toJSON mm
  toJSON (CommonMinimumMatchHighLow (MinimumMatchHighLow lowF highF)) =
    object
      [ "low_freq" .= lowF,
        "high_freq" .= highF
      ]

instance FromJSON CommonMinimumMatch where
  parseJSON v =
    parseMinimum v
      <|> parseMinimumHighLow v
    where
      parseMinimum = fmap CommonMinimumMatch . parseJSON
      parseMinimumHighLow =
        fmap CommonMinimumMatchHighLow
          . withObject
            "CommonMinimumMatchHighLow"
            ( \o ->
                MinimumMatchHighLow
                  <$> o .: "low_freq"
                  <*> o .: "high_freq"
            )

data MinimumMatchHighLow = MinimumMatchHighLow
  { lowFreq :: MinimumMatch,
    highFreq :: MinimumMatch
  }
  deriving (Eq, Show, Generic)

data ZeroTermsQuery
  = ZeroTermsNone
  | ZeroTermsAll
  deriving (Eq, Show, Generic)

instance ToJSON ZeroTermsQuery where
  toJSON ZeroTermsNone = String "none"
  toJSON ZeroTermsAll = String "all"

instance FromJSON ZeroTermsQuery where
  parseJSON = withText "ZeroTermsQuery" parse
    where
      parse "none" = pure ZeroTermsNone
      parse "all" = pure ZeroTermsAll
      parse q = fail ("Unexpected ZeroTermsQuery: " <> show q)

data RangeExecution
  = RangeExecutionIndex
  | RangeExecutionFielddata
  deriving (Eq, Show, Generic)

-- index for smaller ranges, fielddata for longer ranges
instance ToJSON RangeExecution where
  toJSON RangeExecutionIndex = "index"
  toJSON RangeExecutionFielddata = "fielddata"

instance FromJSON RangeExecution where
  parseJSON = withText "RangeExecution" parse
    where
      parse "index" = pure RangeExecutionIndex
      parse "fielddata" = pure RangeExecutionFielddata
      parse t = error ("Unrecognized RangeExecution " <> show t)

newtype Regexp = Regexp Text deriving (Eq, Show, Generic, FromJSON)

data RegexpFlags
  = AllRegexpFlags
  | NoRegexpFlags
  | SomeRegexpFlags (NonEmpty RegexpFlag)
  deriving (Eq, Show, Generic)

instance ToJSON RegexpFlags where
  toJSON AllRegexpFlags = String "ALL"
  toJSON NoRegexpFlags = String "NONE"
  toJSON (SomeRegexpFlags (h :| fs)) = String $ T.intercalate "|" flagStrs
    where
      flagStrs = map flagStr . nub $ h : fs
      flagStr AnyString = "ANYSTRING"
      flagStr Automaton = "AUTOMATON"
      flagStr Complement = "COMPLEMENT"
      flagStr Empty = "EMPTY"
      flagStr Intersection = "INTERSECTION"
      flagStr Interval = "INTERVAL"

instance FromJSON RegexpFlags where
  parseJSON = withText "RegexpFlags" parse
    where
      parse "ALL" = pure AllRegexpFlags
      parse "NONE" = pure NoRegexpFlags
      parse t = SomeRegexpFlags <$> parseNEJSON (String <$> T.splitOn "|" t)

data RegexpFlag
  = AnyString
  | Automaton
  | Complement
  | Empty
  | Intersection
  | Interval
  deriving (Eq, Show, Generic)

instance FromJSON RegexpFlag where
  parseJSON = withText "RegexpFlag" parse
    where
      parse "ANYSTRING" = pure AnyString
      parse "AUTOMATON" = pure Automaton
      parse "COMPLEMENT" = pure Complement
      parse "EMPTY" = pure Empty
      parse "INTERSECTION" = pure Intersection
      parse "INTERVAL" = pure Interval
      parse f = fail ("Unknown RegexpFlag: " <> show f)

newtype LessThan = LessThan Double deriving (Eq, Show, Generic)

newtype LessThanEq = LessThanEq Double deriving (Eq, Show, Generic)

newtype GreaterThan = GreaterThan Double deriving (Eq, Show, Generic)

newtype GreaterThanEq = GreaterThanEq Double deriving (Eq, Show, Generic)

newtype LessThanD = LessThanD UTCTime deriving (Eq, Show, Generic)

newtype LessThanEqD = LessThanEqD UTCTime deriving (Eq, Show, Generic)

newtype GreaterThanD = GreaterThanD UTCTime deriving (Eq, Show, Generic)

newtype GreaterThanEqD = GreaterThanEqD UTCTime deriving (Eq, Show, Generic)

data RangeValue
  = RangeDateLte LessThanEqD
  | RangeDateLt LessThanD
  | RangeDateGte GreaterThanEqD
  | RangeDateGt GreaterThanD
  | RangeDateGtLt GreaterThanD LessThanD
  | RangeDateGteLte GreaterThanEqD LessThanEqD
  | RangeDateGteLt GreaterThanEqD LessThanD
  | RangeDateGtLte GreaterThanD LessThanEqD
  | RangeDoubleLte LessThanEq
  | RangeDoubleLt LessThan
  | RangeDoubleGte GreaterThanEq
  | RangeDoubleGt GreaterThan
  | RangeDoubleGtLt GreaterThan LessThan
  | RangeDoubleGteLte GreaterThanEq LessThanEq
  | RangeDoubleGteLt GreaterThanEq LessThan
  | RangeDoubleGtLte GreaterThan LessThanEq
  deriving (Eq, Show, Generic)

parseRangeValue ::
  ( FromJSON t4,
    FromJSON t3,
    FromJSON t2,
    FromJSON t1
  ) =>
  (t3 -> t5) ->
  (t1 -> t6) ->
  (t4 -> t7) ->
  (t2 -> t8) ->
  (t5 -> t6 -> b) ->
  (t7 -> t6 -> b) ->
  (t5 -> t8 -> b) ->
  (t7 -> t8 -> b) ->
  (t5 -> b) ->
  (t6 -> b) ->
  (t7 -> b) ->
  (t8 -> b) ->
  Parser b ->
  Object ->
  Parser b
parseRangeValue
  mkGt
  mkLt
  mkGte
  mkLte
  fGtLt
  fGteLt
  fGtLte
  fGteLte
  fGt
  fLt
  fGte
  fLte
  nada
  o = do
    lt <- o .:? "lt"
    lte <- o .:? "lte"
    gt <- o .:? "gt"
    gte <- o .:? "gte"
    case (lt, lte, gt, gte) of
      (Just a, _, Just b, _) ->
        return (fGtLt (mkGt b) (mkLt a))
      (Just a, _, _, Just b) ->
        return (fGteLt (mkGte b) (mkLt a))
      (_, Just a, Just b, _) ->
        return (fGtLte (mkGt b) (mkLte a))
      (_, Just a, _, Just b) ->
        return (fGteLte (mkGte b) (mkLte a))
      (_, _, Just a, _) ->
        return (fGt (mkGt a))
      (Just a, _, _, _) ->
        return (fLt (mkLt a))
      (_, _, _, Just a) ->
        return (fGte (mkGte a))
      (_, Just a, _, _) ->
        return (fLte (mkLte a))
      (Nothing, Nothing, Nothing, Nothing) ->
        nada

instance FromJSON RangeValue where
  parseJSON = withObject "RangeValue" parse
    where
      parse o =
        parseDate o
          <|> parseDouble o
      parseDate o =
        parseRangeValue
          GreaterThanD
          LessThanD
          GreaterThanEqD
          LessThanEqD
          RangeDateGtLt
          RangeDateGteLt
          RangeDateGtLte
          RangeDateGteLte
          RangeDateGt
          RangeDateLt
          RangeDateGte
          RangeDateLte
          mzero
          o
      parseDouble o =
        parseRangeValue
          GreaterThan
          LessThan
          GreaterThanEq
          LessThanEq
          RangeDoubleGtLt
          RangeDoubleGteLt
          RangeDoubleGtLte
          RangeDoubleGteLte
          RangeDoubleGt
          RangeDoubleLt
          RangeDoubleGte
          RangeDoubleLte
          mzero
          o

rangeValueToPair :: RangeValue -> [Pair]
rangeValueToPair rv = case rv of
  RangeDateLte (LessThanEqD t) -> ["lte" .= t]
  RangeDateGte (GreaterThanEqD t) -> ["gte" .= t]
  RangeDateLt (LessThanD t) -> ["lt" .= t]
  RangeDateGt (GreaterThanD t) -> ["gt" .= t]
  RangeDateGteLte (GreaterThanEqD l) (LessThanEqD g) -> ["gte" .= l, "lte" .= g]
  RangeDateGtLte (GreaterThanD l) (LessThanEqD g) -> ["gt" .= l, "lte" .= g]
  RangeDateGteLt (GreaterThanEqD l) (LessThanD g) -> ["gte" .= l, "lt" .= g]
  RangeDateGtLt (GreaterThanD l) (LessThanD g) -> ["gt" .= l, "lt" .= g]
  RangeDoubleLte (LessThanEq t) -> ["lte" .= t]
  RangeDoubleGte (GreaterThanEq t) -> ["gte" .= t]
  RangeDoubleLt (LessThan t) -> ["lt" .= t]
  RangeDoubleGt (GreaterThan t) -> ["gt" .= t]
  RangeDoubleGteLte (GreaterThanEq l) (LessThanEq g) -> ["gte" .= l, "lte" .= g]
  RangeDoubleGtLte (GreaterThan l) (LessThanEq g) -> ["gt" .= l, "lte" .= g]
  RangeDoubleGteLt (GreaterThanEq l) (LessThan g) -> ["gte" .= l, "lt" .= g]
  RangeDoubleGtLt (GreaterThan l) (LessThan g) -> ["gt" .= l, "lt" .= g]

data Term = Term
  { termField :: Key,
    termValue :: Text
  }
  deriving (Eq, Show, Generic)

instance ToJSON Term where
  toJSON (Term field value) =
    object
      [ "term"
          .= object
            [field .= value]
      ]

instance FromJSON Term where
  parseJSON = withObject "Term" parse
    where
      parse o = do
        termObj <- o .: "term"
        case HM.toList termObj of
          [(fn, v)] -> Term fn <$> parseJSON v
          _ -> fail "Expected object with 1 field-named key"

data BoolMatch
  = MustMatch Term Cache
  | MustNotMatch Term Cache
  | ShouldMatch [Term] Cache
  deriving (Eq, Show, Generic)

instance ToJSON BoolMatch where
  toJSON (MustMatch term cache) =
    object
      [ "must" .= term,
        "_cache" .= cache
      ]
  toJSON (MustNotMatch term cache) =
    object
      [ "must_not" .= term,
        "_cache" .= cache
      ]
  toJSON (ShouldMatch terms cache) =
    object
      [ "should" .= fmap toJSON terms,
        "_cache" .= cache
      ]

instance FromJSON BoolMatch where
  parseJSON = withObject "BoolMatch" parse
    where
      parse o =
        mustMatch
          `taggedWith` "must"
          <|> mustNotMatch
          `taggedWith` "must_not"
          <|> shouldMatch
          `taggedWith` "should"
        where
          taggedWith parser k = parser =<< o .: k
          mustMatch t = MustMatch t <$> o .:? "_cache" .!= defaultCache
          mustNotMatch t = MustNotMatch t <$> o .:? "_cache" .!= defaultCache
          shouldMatch t = ShouldMatch t <$> o .:? "_cache" .!= defaultCache

-- "memory" or "indexed"
data GeoFilterType
  = GeoFilterMemory
  | GeoFilterIndexed
  deriving (Eq, Show, Generic)

instance ToJSON GeoFilterType where
  toJSON GeoFilterMemory = String "memory"
  toJSON GeoFilterIndexed = String "indexed"

instance FromJSON GeoFilterType where
  parseJSON = withText "GeoFilterType" parse
    where
      parse "memory" = pure GeoFilterMemory
      parse "indexed" = pure GeoFilterIndexed
      parse t = fail ("Unrecognized GeoFilterType: " <> show t)

data LatLon = LatLon
  { lat :: Double,
    lon :: Double
  }
  deriving (Eq, Show, Generic)

instance ToJSON LatLon where
  toJSON (LatLon lLat lLon) =
    object
      [ "lat" .= lLat,
        "lon" .= lLon
      ]

instance FromJSON LatLon where
  parseJSON = withObject "LatLon" parse
    where
      parse o =
        LatLon
          <$> o .: "lat"
          <*> o .: "lon"

data GeoBoundingBox = GeoBoundingBox
  { topLeft :: LatLon,
    bottomRight :: LatLon
  }
  deriving (Eq, Show, Generic)

instance ToJSON GeoBoundingBox where
  toJSON (GeoBoundingBox gbbTopLeft gbbBottomRight) =
    object
      [ "top_left" .= gbbTopLeft,
        "bottom_right" .= gbbBottomRight
      ]

instance FromJSON GeoBoundingBox where
  parseJSON = withObject "GeoBoundingBox" parse
    where
      parse o =
        GeoBoundingBox
          <$> o .: "top_left"
          <*> o .: "bottom_right"

data GeoBoundingBoxConstraint = GeoBoundingBoxConstraint
  { geoBBField :: FieldName,
    constraintBox :: GeoBoundingBox,
    bbConstraintcache :: Cache,
    geoType :: GeoFilterType
  }
  deriving (Eq, Show, Generic)

instance ToJSON GeoBoundingBoxConstraint where
  toJSON
    ( GeoBoundingBoxConstraint
        (FieldName gbbcGeoBBField)
        gbbcConstraintBox
        cache
        type'
      ) =
      object
        [ fromText gbbcGeoBBField .= gbbcConstraintBox,
          "_cache" .= cache,
          "type" .= type'
        ]

instance FromJSON GeoBoundingBoxConstraint where
  parseJSON = withObject "GeoBoundingBoxConstraint" parse
    where
      parse o = case X.toList (deleteSeveral ["type", "_cache"] o) of
        [(fn, v)] ->
          GeoBoundingBoxConstraint (FieldName $ toText fn)
            <$> parseJSON v
            <*> o .:? "_cache" .!= defaultCache
            <*> o .: "type"
        _ -> fail "Could not find field name for GeoBoundingBoxConstraint"

data GeoPoint = GeoPoint
  { geoField :: FieldName,
    latLon :: LatLon
  }
  deriving (Eq, Show, Generic)

instance ToJSON GeoPoint where
  toJSON (GeoPoint (FieldName geoPointField) geoPointLatLon) =
    object [fromText geoPointField .= geoPointLatLon]

data DistanceUnit
  = Miles
  | Yards
  | Feet
  | Inches
  | Kilometers
  | Meters
  | Centimeters
  | Millimeters
  | NauticalMiles
  deriving (Eq, Show, Generic)

instance ToJSON DistanceUnit where
  toJSON Miles = String "mi"
  toJSON Yards = String "yd"
  toJSON Feet = String "ft"
  toJSON Inches = String "in"
  toJSON Kilometers = String "km"
  toJSON Meters = String "m"
  toJSON Centimeters = String "cm"
  toJSON Millimeters = String "mm"
  toJSON NauticalMiles = String "nmi"

instance FromJSON DistanceUnit where
  parseJSON = withText "DistanceUnit" parse
    where
      parse "mi" = pure Miles
      parse "yd" = pure Yards
      parse "ft" = pure Feet
      parse "in" = pure Inches
      parse "km" = pure Kilometers
      parse "m" = pure Meters
      parse "cm" = pure Centimeters
      parse "mm" = pure Millimeters
      parse "nmi" = pure NauticalMiles
      parse u = fail ("Unrecognized DistanceUnit: " <> show u)

data DistanceType
  = Arc
  | SloppyArc -- doesn't exist <1.0
  | Plane
  deriving (Eq, Show, Generic)

instance ToJSON DistanceType where
  toJSON Arc = String "arc"
  toJSON SloppyArc = String "sloppy_arc"
  toJSON Plane = String "plane"

instance FromJSON DistanceType where
  parseJSON = withText "DistanceType" parse
    where
      parse "arc" = pure Arc
      parse "sloppy_arc" = pure SloppyArc
      parse "plane" = pure Plane
      parse t = fail ("Unrecognized DistanceType: " <> show t)

data OptimizeBbox
  = OptimizeGeoFilterType GeoFilterType
  | NoOptimizeBbox
  deriving (Eq, Show, Generic)

instance ToJSON OptimizeBbox where
  toJSON NoOptimizeBbox = String "none"
  toJSON (OptimizeGeoFilterType gft) = toJSON gft

instance FromJSON OptimizeBbox where
  parseJSON v =
    withText "NoOptimizeBbox" parseNoOptimize v
      <|> parseOptimize v
    where
      parseNoOptimize "none" = pure NoOptimizeBbox
      parseNoOptimize _ = mzero
      parseOptimize = fmap OptimizeGeoFilterType . parseJSON

data Distance = Distance
  { coefficient :: Double,
    unit :: DistanceUnit
  }
  deriving (Eq, Show, Generic)

instance ToJSON Distance where
  toJSON (Distance dCoefficient dUnit) =
    String boltedTogether
    where
      coefText = showText dCoefficient
      (String unitText) = toJSON dUnit
      boltedTogether = mappend coefText unitText

instance FromJSON Distance where
  parseJSON = withText "Distance" parse
    where
      parse t =
        Distance
          <$> parseCoeff nT
          <*> parseJSON (String unitT)
        where
          (nT, unitT) = T.span validForNumber t
          -- may be a better way to do this
          validForNumber '-' = True
          validForNumber '.' = True
          validForNumber 'e' = True
          validForNumber c = isNumber c
          parseCoeff "" = fail "Empty string cannot be parsed as number"
          parseCoeff s = return (read (T.unpack s))

data DistanceRange = DistanceRange
  { distanceFrom :: Distance,
    distanceTo :: Distance
  }
  deriving (Eq, Show, Generic)

type TemplateQueryValue = Text

newtype TemplateQueryKeyValuePairs
  = TemplateQueryKeyValuePairs (X.KeyMap TemplateQueryValue)
  deriving (Eq, Show)

instance ToJSON TemplateQueryKeyValuePairs where
  toJSON (TemplateQueryKeyValuePairs x) = Object $ String <$> x

instance FromJSON TemplateQueryKeyValuePairs where
  parseJSON (Object o) =
    pure . TemplateQueryKeyValuePairs $ X.mapMaybe getValue o
    where
      getValue (String x) = Just x
      getValue _ = Nothing
  parseJSON _ =
    fail "error parsing TemplateQueryKeyValuePairs"

-- | 'BooleanOperator' is the usual And/Or operators with an ES compatible
--    JSON encoding baked in. Used all over the place.
data BooleanOperator = And | Or deriving (Eq, Show, Generic)

instance ToJSON BooleanOperator where
  toJSON And = String "and"
  toJSON Or = String "or"

instance FromJSON BooleanOperator where
  parseJSON = withText "BooleanOperator" parse
    where
      parse "and" = pure And
      parse "or" = pure Or
      parse o = fail ("Unexpected BooleanOperator: " <> show o)

-- | 'Cache' is for telling ES whether it should cache a 'Filter' not.
--    'Query's cannot be cached.
type Cache = Bool -- caching on/off

defaultCache :: Cache
defaultCache = False

data FunctionScoreQuery = FunctionScoreQuery
  { functionScoreQuery :: Maybe Query,
    functionScoreBoost :: Maybe Boost,
    functionScoreFunctions :: FunctionScoreFunctions,
    functionScoreMaxBoost :: Maybe Boost,
    functionScoreBoostMode :: Maybe BoostMode,
    functionScoreMinScore :: Score,
    functionScoreScoreMode :: Maybe ScoreMode
  }
  deriving (Eq, Show, Generic)

instance ToJSON FunctionScoreQuery where
  toJSON (FunctionScoreQuery query boost fns maxBoost boostMode minScore scoreMode) =
    omitNulls base
    where
      base =
        functionScoreFunctionsPair fns
          : [ "query" .= query,
              "boost" .= boost,
              "max_boost" .= maxBoost,
              "boost_mode" .= boostMode,
              "min_score" .= minScore,
              "score_mode" .= scoreMode
            ]

instance FromJSON FunctionScoreQuery where
  parseJSON = withObject "FunctionScoreQuery" parse
    where
      parse o =
        FunctionScoreQuery
          <$> o .:? "query"
          <*> o .:? "boost"
          <*> ( singleFunction o
                  <|> multipleFunctions
                  `taggedWith` "functions"
              )
          <*> o .:? "max_boost"
          <*> o .:? "boost_mode"
          <*> o .:? "min_score"
          <*> o .:? "score_mode"
        where
          taggedWith parser k = parser =<< o .: k
      singleFunction = fmap FunctionScoreSingle . parseFunctionScoreFunction
      multipleFunctions = pure . FunctionScoreMultiple

data FunctionScoreFunctions
  = FunctionScoreSingle FunctionScoreFunction
  | FunctionScoreMultiple (NonEmpty ComponentFunctionScoreFunction)
  deriving (Eq, Show, Generic)

data ComponentFunctionScoreFunction = ComponentFunctionScoreFunction
  { componentScoreFunctionFilter :: Maybe Filter,
    componentScoreFunction :: FunctionScoreFunction,
    componentScoreFunctionWeight :: Maybe Weight
  }
  deriving (Eq, Show, Generic)

instance ToJSON ComponentFunctionScoreFunction where
  toJSON (ComponentFunctionScoreFunction filter' fn weight) =
    omitNulls base
    where
      base =
        functionScoreFunctionPair fn
          : [ "filter" .= filter',
              "weight" .= weight
            ]

instance FromJSON ComponentFunctionScoreFunction where
  parseJSON = withObject "ComponentFunctionScoreFunction" parse
    where
      parse o =
        ComponentFunctionScoreFunction
          <$> o .:? "filter"
          <*> parseFunctionScoreFunction o
          <*> o .:? "weight"

functionScoreFunctionsPair :: FunctionScoreFunctions -> (Key, Value)
functionScoreFunctionsPair (FunctionScoreSingle fn) =
  functionScoreFunctionPair fn
functionScoreFunctionsPair (FunctionScoreMultiple componentFns) =
  ("functions", toJSON componentFns)

fieldTagged :: (Monad m, MonadFail m) => (FieldName -> Object -> m a) -> Object -> m a
fieldTagged f o = case X.toList o of
  [(k, Object o')] -> f (FieldName $ toText k) o'
  _ -> fail "Expected object with 1 field-named key"

-- | Fuzziness value as a number or 'AUTO'.
-- See:
-- https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#fuzziness
data Fuzziness = Fuzziness Double | FuzzinessAuto
  deriving (Eq, Show, Generic)

instance ToJSON Fuzziness where
  toJSON (Fuzziness n) = toJSON n
  toJSON FuzzinessAuto = String "AUTO"

instance FromJSON Fuzziness where
  parseJSON (String "AUTO") = return FuzzinessAuto
  parseJSON v = Fuzziness <$> parseJSON v

data InnerHits = InnerHits
  { innerHitsFrom :: Maybe Integer,
    innerHitsSize :: Maybe Integer
  }
  deriving (Eq, Show, Generic)

instance ToJSON InnerHits where
  toJSON (InnerHits ihFrom ihSize) =
    omitNulls
      [ "from" .= ihFrom,
        "size" .= ihSize
      ]

instance FromJSON InnerHits where
  parseJSON = withObject "InnerHits" parse
    where
      parse o =
        InnerHits
          <$> o .:? "from"
          <*> o .:? "size"