packages feed

bloodhound-1.0.0.0: src/Database/Bloodhound/Common/Requests.hs

{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}

-- |
-- Module : Database.Bloodhound.Common.Requests
-- Copyright : (C) 2014, 2018 Chris Allen
-- License : BSD-style (see the file LICENSE)
-- Maintainer : Chris Allen <cma@bitemyapp.com>
-- Stability : provisional
-- Portability : GHC
--
-- Request to be run against Elasticsearch servers..
module Database.Bloodhound.Common.Requests
  ( -- * Bloodhound client functions

    -- ** Indices
    createIndex,
    createIndexOptions,
    createIndexOptionsBody,
    createIndexOptionsParams,
    createIndexWith,
    flushIndex,
    flushIndexWith,
    clearIndexCache,
    reloadSearchAnalyzers,
    reloadSearchAnalyzersWith,
    diskUsage,
    diskUsageWith,
    fieldUsageStats,
    fieldUsageStatsWith,
    getScriptContexts,
    getScriptLanguages,
    deleteIndex,
    updateIndexSettings,
    updateIndexSettingsWith,
    getIndexSettings,
    getIndexSettingsWith,
    getIndex,
    getIndexStats,
    getIndexRecovery,
    getIndexSegments,
    getShardStores,
    getShardStoresWith,
    ShardStoresOptions (..),
    defaultShardStoresOptions,
    shardStoresOptionsParams,
    forceMergeIndex,
    indexExists,
    doesExist,
    openIndex,
    openIndexWith,
    closeIndex,
    closeIndexWith,
    listIndices,
    catIndices,
    catIndicesWith,
    catAliases,
    catAliasesWith,
    catAllocation,
    catAllocationWith,
    catCount,
    catCountWith,
    catMaster,
    catMasterWith,
    catHealth,
    catHealthWith,
    catPendingTasks,
    catPendingTasksWith,
    catPlugins,
    catPluginsWith,
    catTemplates,
    catTemplatesWith,
    catThreadPool,
    catThreadPoolWith,
    catFielddata,
    catFielddataWith,
    catNodeattrs,
    catNodeattrsWith,
    catRepositories,
    catRepositoriesWith,
    catShards,
    catShardsWith,
    catTasks,
    catTasksWith,
    catSnapshots,
    catSnapshotsWith,
    catNodes,
    catNodesWith,
    catSegments,
    catSegmentsWith,
    catRecovery,
    catRecoveryWith,
    catComponentTemplates,
    catComponentTemplatesWith,
    catCircuitBreakers,
    catCircuitBreakersWith,
    catMlJobs,
    catMlJobsWith,
    catMlDataFrameAnalytics,
    catMlDataFrameAnalyticsWith,
    catMlDatafeeds,
    catMlDatafeedsWith,
    catMlTrainedModels,
    catMlTrainedModelsWith,
    catTransforms,
    catTransformsWith,
    catHelp,
    addIndexBlock,
    removeIndexBlock,
    waitForYellowIndex,
    rolloverIndex,
    resolveIndex,
    resolveIndexWith,
    shrinkIndex,
    splitIndex,
    cloneIndex,
    HealthStatus (..),

    -- *** Dangling indices
    listDanglingIndices,
    importDanglingIndex,
    importDanglingIndexWith,
    deleteDanglingIndex,
    deleteDanglingIndexWith,
    DanglingIndexUuid (..),
    unDanglingIndexUuid,
    DanglingIndex (..),
    DanglingIndicesResponse (..),
    ImportDanglingIndexOptions (..),
    defaultImportDanglingIndexOptions,
    importDanglingIndexOptionsParams,
    DeleteDanglingIndexOptions (..),
    defaultDeleteDanglingIndexOptions,
    deleteDanglingIndexOptionsParams,

    -- *** Index Aliases
    updateIndexAliases,
    updateIndexAliasesWith,
    UpdateAliasesOptions (..),
    defaultUpdateAliasesOptions,
    updateAliasesOptionsParams,
    createIndexAlias,
    getIndexAliases,
    getIndexAlias,
    deleteIndexAlias,
    deleteIndexAliasFrom,
    aliasExists,
    defaultIndexAliasCreate,

    -- *** Index Templates
    putTemplate,
    templateExists,
    getTemplate,
    deleteTemplate,
    ComponentTemplate (..),
    ComposableTemplate (..),
    ComposableTemplateContent (..),
    ComposableTemplateOptions (..),
    defaultComposableTemplateOptions,
    composableTemplateOptionsParams,
    putIndexTemplate,
    putIndexTemplateWith,
    getIndexTemplate,
    deleteIndexTemplate,
    simulateIndexTemplate,
    simulateIndexTemplateWith,
    simulateIndex,
    simulateIndexWith,
    putComponentTemplate,
    deleteComponentTemplate,
    IndexTemplateInfo (..),
    GetIndexTemplatesResponse (..),
    TemplateInfo (..),
    GetTemplatesResponse (..),
    getComponentTemplate,
    ComponentTemplateInfo (..),
    GetComponentTemplatesResponse (..),
    TemplateNamePattern (..),
    SimulatedTemplate (..),
    SimulatedTemplateOverlap (..),

    -- ** Mapping
    putMapping,
    putMappingWith,
    getMapping,
    getFieldMapping,

    -- ** Documents
    indexDocument,
    updateDocument,
    updateDocumentWith,
    updateByQuery,
    updateByQueryWith,
    rethrottleUpdateByQuery,
    getDocument,
    getDocumentWith,
    getDocumentSource,
    getDocumentSourceWith,
    getDocuments,
    getDocumentsWith,
    getDocumentsMulti,
    getDocumentsMultiWith,
    getTermVectors,
    getTermVectorsWith,
    getMultiTermVectors,
    getMultiTermVectorsWith,
    getMultiTermVectorsByIndex,
    getMultiTermVectorsByIndexWith,
    documentExists,
    documentExistsWith,
    documentSourceExists,
    documentSourceExistsWith,
    deleteDocument,
    deleteDocumentWith,
    deleteByQuery,
    deleteByQueryWith,
    rethrottleDeleteByQuery,
    getDocumentOptionsParams,
    getDocumentSourceOptionsParams,
    multiGetOptionsParams,
    deleteDocumentOptionsParams,
    documentExistsOptionsParams,
    documentSourceExistsParams,
    byQueryOptionsParams,
    termVectorsOptionsParams,
    explainOptionsParams,
    IndexedDocument (..),
    DeletedDocuments (..),
    DeletedDocumentsRetries (..),

    -- ** Searching
    searchAll,
    searchAllWith,
    multiSearch,
    multiSearchWith,
    multiSearchByIndex,
    multiSearchByIndexWith,
    MultiSearchTemplateItem (..),
    multiSearchTemplate,
    multiSearchTemplateWith,
    multiSearchTemplateByIndex,
    multiSearchTemplateByIndexWith,
    searchByIndex,
    searchByIndexWith,
    searchByIndices,
    searchByIndicesWith,
    explainDocument,
    explainDocumentWith,
    searchByIndexTemplate,
    searchByIndicesTemplate,
    getInitialScroll,
    getInitialSortedScroll,
    advanceScroll,
    advanceScrollWith,
    clearScroll,
    refreshIndex,
    refreshIndexWith,
    mkSearch,
    mkAggregateSearch,
    mkHighlightSearch,
    mkSearchTemplate,
    bulk,
    bulkWith,
    pageSearch,
    mkShardCount,
    mkReplicaCount,
    getStatus,
    dispatchSearch,
    dispatchSearchWith,
    searchOptionsParams,

    -- ** Templates
    storeSearchTemplate,
    getSearchTemplate,
    deleteSearchTemplate,
    renderTemplate,

    -- ** Snapshot/Restore

    -- *** Snapshot Repos
    getSnapshotRepos,
    getSnapshotReposWith,
    updateSnapshotRepo,
    verifySnapshotRepo,
    verifySnapshotRepoWith,
    cleanupSnapshotRepo,
    cleanupSnapshotRepoWith,
    deleteSnapshotRepo,
    deleteSnapshotRepoWith,
    snapshotMasterTimeoutOptionsParams,

    -- *** Snapshots
    createSnapshot,
    cloneSnapshot,
    getSnapshots,
    getSnapshotsWith,
    snapshotSelectionOptionsParams,
    SIs (..),
    getSnapshotStatus,
    getSnapshotStatusWith,
    deleteSnapshot,
    deleteSnapshotWith,

    -- *** Restoring Snapshots
    restoreSnapshot,

    -- *** Reindex
    reindex,
    reindexWith,
    reindexAsync,
    reindexAsyncWith,
    rethrottleReindex,

    -- *** Task
    getTask,
    getTaskWith,
    cancelTask,
    listTasks,
    taskListOptionsParams,
    taskGetOptionsParams,

    -- ** Async Search
    getAsyncSearch,
    getAsyncSearchStatus,

    -- ** Nodes
    getNodesInfo,
    getNodesInfoWith,
    getNodesStats,
    getNodesStatsWith,
    getNodesUsage,
    getNodesUsageWith,
    getNodesHotThreads,
    getNodesHotThreadsWith,
    hotThreadsOptionsParams,
    reloadSecureSettings,

    -- ** Cluster
    getClusterHealth,
    getClusterHealthWith,
    getClusterHealthForIndex,
    getClusterHealthForIndexWith,
    clusterHealthOptionsParams,
    getClusterSettings,
    getClusterSettingsWith,
    clusterSettingsOptionsParams,
    updateClusterSettings,
    updateClusterSettingsWith,
    clusterSettingsUpdateOptionsParams,
    getClusterState,
    getClusterStateWith,
    clusterStateOptionsParams,
    clusterStateOptionsPathSegments,
    getClusterStats,
    getPendingTasks,
    explainAllocation,
    getRemoteClusterInfo,
    updateVotingConfigExclusions,
    votingConfigExclusionOptionsParams,
    clearVotingConfigExclusions,
    rerouteCluster,
    rerouteClusterWith,
    rerouteOptionsParams,

    -- ** Index Lifecycle Management (ILM)
    getILMPolicy,
    putILMPolicy,
    deleteILMPolicy,
    explainILM,
    explainILMWith,
    ilmExplainOptionsParams,
    startILM,
    stopILM,
    getILMStatus,
    moveILMStep,
    retryILMStep,
    removeILM,
    migrateDataTiers,
    migrateDataTiersWith,

    -- ** Snapshot Lifecycle Management (SLM)
    putSLMPolicy,
    getSLMPolicy,
    deleteSLMPolicy,
    executeSLMPolicy,
    getSLMStatus,
    startSLM,
    stopSLM,

    -- ** Enrich
    putEnrichPolicy,
    getEnrichPolicy,
    deleteEnrichPolicy,
    executeEnrichPolicy,
    executeEnrichPolicyWith,
    getEnrichExecuteStats,
    getEnrichPolicyExecuteStats,

    -- ** Autoscaling
    putAutoscalingPolicy,
    getAutoscalingPolicy,
    deleteAutoscalingPolicy,
    getAutoscalingCapacity,

    -- ** Security — roles (/_security/role/*)
    putRole,
    getRole,
    getRoles,
    deleteRole,
    clearRoleCache,

    -- ** Security — role mappings (/_security/role_mapping/*)
    putRoleMapping,
    getRoleMapping,
    getRoleMappings,
    deleteRoleMapping,

    -- ** Security — users (/_security/user/*)
    putUser,
    getUser,
    getUsers,
    deleteUser,
    enableUser,
    disableUser,
    changeUserPassword,
    userHasPrivileges,
    selfHasPrivileges,
    getUserPrivileges,

    -- ** Security — API keys (/_security/api_key)
    createApiKey,
    grantApiKey,
    getApiKey,
    invalidateApiKey,
    updateApiKey,
    queryApiKey,
    queryApiKeyWith,
    QueryApiKeyOptions (..),
    defaultQueryApiKeyOptions,

    -- ** Security — authentication (/_security/_authenticate, _whoami, _logout)
    authenticate,
    whoami,
    logout,

    -- ** Security — service accounts (/_security/service/*)
    getServiceAccounts,
    getServiceAccountsInNamespace,
    getServiceAccount,
    createServiceToken,
    getServiceCredentials,
    deleteServiceToken,

    -- ** Security — SSO / token (/_security/oauth2, /_security/oidc, /_security/saml)
    getToken,
    invalidateToken,
    prepareSamlAuthentication,
    authenticateSaml,
    logoutSaml,
    prepareOidcAuthentication,
    authenticateOidc,
    logoutOidc,

    -- ** Security — application privileges (/_security/privilege/*)
    putPrivilege,
    getPrivileges,
    getPrivilegesInApplication,
    getPrivilege,
    deletePrivilege,
    clearPrivilegeCache,

    -- ** Fleet
    getFleetGlobalCheckpoints,
    getFleetGlobalCheckpointsWith,
    fleetSearch,
    fleetSearchWith,
    fleetMultiSearch,
    fleetMultiSearchWith,
    FleetGlobalCheckpointsOptions (..),
    defaultFleetGlobalCheckpointsOptions,
    FleetSearchOptions (..),
    defaultFleetSearchOptions,

    -- ** Repositories Metering
    getRepositoriesMetering,
    deleteRepositoriesMetering,

    -- ** Rollup
    putRollupJob,
    getRollupJob,
    deleteRollupJob,
    startRollupJob,
    stopRollupJob,
    stopRollupJobWith,
    getRollupJobStats,
    getRollupCapabilities,
    getRollupIndexCapabilities,
    rollupSearchByIndex,
    rollupSearchByIndexWith,

    -- ** Searchable Snapshots
    mountSearchableSnapshot,
    mountSearchableSnapshotWith,
    getSearchableSnapshotsStats,
    getSearchableSnapshotsCacheStats,
    clearSearchableSnapshotsCache,

    -- ** Watcher
    putWatch,
    getWatch,
    deleteWatch,
    executeWatch,
    executeWatchWith,
    ackWatch,
    activateWatch,
    deactivateWatch,
    watcherStats,
    watcherStatsWith,
    getWatcherSettings,
    updateWatcherSettings,
    startWatcher,
    stopWatcher,

    -- ** Text structure
    findStructure,
    findStructureWith,
    findMessageStructure,
    findMessageStructureWith,
    findFieldStructure,
    findFieldStructureWith,
    testGrokPattern,
    testGrokPatternWith,

    -- ** Transform
    putTransform,
    putTransformWith,
    updateTransform,
    updateTransformWith,
    getTransforms,
    getTransformsWith,
    deleteTransform,
    deleteTransformWith,
    startTransform,
    startTransformWith,
    stopTransform,
    stopTransformWith,
    previewTransform,
    previewTransformWith,
    getTransformStats,
    getTransformStatsWith,
    explainTransform,

    -- ** Migration
    getMigrationDeprecations,
    getSystemFeatures,
    upgradeSystemFeatures,

    -- ** Ingest Pipelines
    putIngestPipeline,
    getIngestPipeline,
    deleteIngestPipeline,
    simulateIngestPipeline,
    getGrokPatterns,

    -- ** Async Search
    deleteAsyncSearch,
    submitAsyncSearch,
    submitAsyncSearchWith,

    -- ** Request Utilities
    encodeBulkOperations,
    encodeBulkOperation,
    SSs (..),
    indexQueryString,

    -- * BHResponse-handling tools
    isVersionConflict,
    isSuccess,
    isCreated,
    parseEsResponse,
    parseEsResponseWith,
    decodeResponse,
    eitherDecodeResponse,

    -- * Count
    countByIndex,
    countByIndexWith,
    countAll,

    -- * Validate
    validateQuery,
    validateQueryWith,
    validateAll,

    -- * Rank Evaluation
    evaluateRank,
    evaluateRankByIndex,
    evaluateRankWith,

    -- * Analyze
    analyzeText,

    -- * Field Capabilities
    getFieldCaps,
    getFieldCapsWith,

    -- * Search Shards
    getSearchShards,
    getSearchShardsWith,
    SearchShardsOptions (..),
    defaultSearchShardsOptions,
    searchShardsOptionsParams,

    -- * Vector Tile (Mapbox Vector Tile search)
    searchVectorTile,
    searchVectorTileWith,
    VectorTileOptions (..),
    defaultVectorTileOptions,
    vectorTileOptionsParams,
    TileZoom (..),
    TileX (..),
    TileY (..),
    VectorTileGridAgg (..),
    VectorTileGridType (..),
    VectorTileTrackTotalHits (..),

    -- * Generic
    Acknowledged (..),
    Accepted (..),
    IgnoredBody (..),

    -- * Performing Requests
    tryPerformBHRequest,
    performBHRequest,
    withBHResponse,
    withBHResponse_,
    withBHResponseParsedEsResponse,
    keepBHResponse,
    joinBHResponse,
  )
where

import Control.Applicative as A
import Control.Monad
import Data.Aeson
import Data.Aeson.Key
import Data.Aeson.KeyMap qualified as X
import Data.ByteString.Builder
import Data.ByteString.Lazy.Char8 qualified as L
import Data.Coerce (coerce)
import Data.Foldable (toList)
import Data.List qualified as LS (foldl')
import Data.List.NonEmpty (NonEmpty (..))
import Data.Maybe (catMaybes, fromMaybe, isNothing)
import Data.Monoid
import Data.Text (Text)
import Data.Text qualified as T
import Data.Time.Clock
import Data.Vector qualified as V
import Database.Bloodhound.Client.Cluster
import Database.Bloodhound.Common.Types
import Database.Bloodhound.Internal.Utils.Imports (showText)
import Database.Bloodhound.Internal.Utils.Requests
import Network.HTTP.Client (responseBody)
import Network.HTTP.Types.Method qualified as NHTM
import Prelude hiding (filter, head)

-- | 'mkShardCount' is a straight-forward smart constructor for 'ShardCount'
--  which rejects 'Int' values below 1 and above 1000.
--
-- >>> mkShardCount 10
-- Just (ShardCount 10)
mkShardCount :: Int -> Maybe ShardCount
mkShardCount n
  | n < 1 = Nothing
  | n > 1000 = Nothing
  | otherwise = Just (ShardCount n)

-- | 'mkReplicaCount' is a straight-forward smart constructor for 'ReplicaCount'
--  which rejects 'Int' values below 0 and above 1000.
--
-- >>> mkReplicaCount 10
-- Just (ReplicaCount 10)
mkReplicaCount :: Int -> Maybe ReplicaCount
mkReplicaCount n
  | n < 0 = Nothing
  | n > 1000 = Nothing -- ...
  | otherwise = Just (ReplicaCount n)

-- | 'getStatus' fetches the 'Status' of a 'Server'
--
-- >>> serverStatus <- runBH' getStatus
-- >>> fmap tagline (serverStatus)
-- Just "You Know, for Search"
getStatus :: BHRequest StatusDependant Status
getStatus = get []

-- | 'getSnapshotRepos' gets the definitions of a subset of the
-- defined snapshot repos.
getSnapshotRepos :: SnapshotRepoSelection -> BHRequest StatusDependant [GenericSnapshotRepo]
getSnapshotRepos sel = getSnapshotReposWith sel defaultSnapshotRepoGetOptions

-- | Variant of 'getSnapshotRepos' that accepts
-- 'SnapshotRepoGetOptions' for the @master_timeout@ and @local@ URI
-- parameters. Calling with 'defaultSnapshotRepoGetOptions' is
-- byte-for-byte identical to 'getSnapshotRepos'.
getSnapshotReposWith ::
  SnapshotRepoSelection ->
  SnapshotRepoGetOptions ->
  BHRequest StatusDependant [GenericSnapshotRepo]
getSnapshotReposWith sel opts =
  unGSRs <$> get (["_snapshot", selectorSeg] `withQueries` snapshotRepoGetOptionsParams opts)
  where
    selectorSeg = case sel of
      AllSnapshotRepos -> "_all"
      SnapshotRepoList (p :| ps) -> T.intercalate "," (renderPat <$> (p : ps))
    renderPat (RepoPattern t) = t
    renderPat (ExactRepo (SnapshotRepoName t)) = t

-- | Wrapper to extract the list of 'GenericSnapshotRepo' in the
-- format they're returned in
newtype GSRs = GSRs {unGSRs :: [GenericSnapshotRepo]}

instance FromJSON GSRs where
  parseJSON = withObject "Collection of GenericSnapshotRepo" parse
    where
      parse = fmap GSRs . mapM (uncurry go) . X.toList
      go rawName = withObject "GenericSnapshotRepo" $ \o ->
        GenericSnapshotRepo (SnapshotRepoName $ toText rawName)
          <$> o
            .: "type"
          <*> o
            .: "settings"

-- | Create or update a snapshot repo
updateSnapshotRepo ::
  (SnapshotRepo repo) =>
  -- | Use 'defaultSnapshotRepoUpdateSettings' if unsure
  SnapshotRepoUpdateSettings ->
  repo ->
  BHRequest StatusIndependant Acknowledged
updateSnapshotRepo SnapshotRepoUpdateSettings {..} repo =
  put endpoint (encode body)
  where
    endpoint = ["_snapshot", snapshotRepoName gSnapshotRepoName] `withQueries` params
    params =
      catMaybes
        [ if repoUpdateVerify then Nothing else Just ("verify", Just "false"),
          ("master_timeout",) . Just . renderDuration <$> repoUpdateMasterTimeout,
          ("timeout",) . Just . renderDuration <$> repoUpdateTimeout
        ]
    body =
      object
        [ "type" .= gSnapshotRepoType,
          "settings" .= gSnapshotRepoSettings
        ]
    GenericSnapshotRepo {..} = toGSnapshotRepo repo
    renderDuration (u, n) = showText n <> timeUnitsSuffix u

-- | Verify if a snapshot repo is working. __NOTE:__ this API did not
-- make it into Elasticsearch until 1.4. If you use an older version,
-- you will get an error here.
verifySnapshotRepo :: SnapshotRepoName -> BHRequest StatusDependant SnapshotVerification
verifySnapshotRepo repoName = verifySnapshotRepoWith repoName defaultSnapshotRepoTimeoutOptions

-- | Variant of 'verifySnapshotRepo' that accepts
-- 'SnapshotRepoTimeoutOptions' for the @master_timeout@ and
-- @timeout@ URI parameters. Calling with
-- 'defaultSnapshotRepoTimeoutOptions' is byte-for-byte identical to
-- 'verifySnapshotRepo'.
verifySnapshotRepoWith ::
  SnapshotRepoName ->
  SnapshotRepoTimeoutOptions ->
  BHRequest StatusDependant SnapshotVerification
verifySnapshotRepoWith (SnapshotRepoName n) opts =
  post (["_snapshot", n, "_verify"] `withQueries` snapshotRepoTimeoutOptionsParams opts) emptyBody

-- | Removes stale data from a snapshot repository (indices' stale
-- segments not referenced by any snapshot). Pinned to
-- 'StatusDependant' so a 404 (missing repository) surfaces as an
-- 'EsError'. The wire format is identical across Elasticsearch and
-- OpenSearch, so this builder is shared by every versioned client
-- via "Database.Bloodhound.Common.Client".
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/clean-up-snapshot-repo-api.html>,
-- <https://docs.opensearch.org/latest/api-reference/snapshots/clean-up-snapshot-repository/>)
cleanupSnapshotRepo ::
  SnapshotRepoName ->
  BHRequest StatusDependant SnapshotCleanupResult
cleanupSnapshotRepo repoName = cleanupSnapshotRepoWith repoName defaultSnapshotMasterTimeoutOptions

-- | Variant of 'cleanupSnapshotRepo' that accepts
-- 'SnapshotMasterTimeoutOptions' for the @master_timeout@ URI
-- parameter. Calling with 'defaultSnapshotMasterTimeoutOptions' is
-- byte-for-byte identical to 'cleanupSnapshotRepo'.
cleanupSnapshotRepoWith ::
  SnapshotRepoName ->
  SnapshotMasterTimeoutOptions ->
  BHRequest StatusDependant SnapshotCleanupResult
cleanupSnapshotRepoWith (SnapshotRepoName n) opts =
  post (["_snapshot", n, "_cleanup"] `withQueries` snapshotMasterTimeoutOptionsParams opts) emptyBody

deleteSnapshotRepo :: SnapshotRepoName -> BHRequest StatusIndependant Acknowledged
deleteSnapshotRepo repoName = deleteSnapshotRepoWith repoName defaultSnapshotRepoTimeoutOptions

-- | Variant of 'deleteSnapshotRepo' that accepts
-- 'SnapshotRepoTimeoutOptions' for the @master_timeout@ and
-- @timeout@ URI parameters. Calling with
-- 'defaultSnapshotRepoTimeoutOptions' is byte-for-byte identical to
-- 'deleteSnapshotRepo'.
deleteSnapshotRepoWith ::
  SnapshotRepoName ->
  SnapshotRepoTimeoutOptions ->
  BHRequest StatusIndependant Acknowledged
deleteSnapshotRepoWith (SnapshotRepoName n) opts =
  delete (["_snapshot", n] `withQueries` snapshotRepoTimeoutOptionsParams opts)

-- | Create and start a snapshot. The response type
-- 'CreateSnapshotResponse' reflects the 'snapWaitForCompletion'
-- setting: when @false@ (the default) the server returns
-- @{"acknowledged": <bool>}@ (decoded to
-- 'CreateSnapshotAcknowledged'); when @true@ it returns a full
-- snapshot object @{"snapshot": {…}}@ (decoded to
-- 'CreateSnapshotCompleted' wrapping a 'SnapshotResponse'). See the
-- create-snapshot API docs for the two response shapes.
createSnapshot ::
  SnapshotRepoName ->
  SnapshotName ->
  SnapshotCreateSettings ->
  BHRequest StatusIndependant CreateSnapshotResponse
createSnapshot (SnapshotRepoName repoName) (SnapshotName snapName) SnapshotCreateSettings {..} =
  post endpoint body
  where
    endpoint = ["_snapshot", repoName, snapName] `withQueries` params
    params =
      catMaybes
        [ Just ("wait_for_completion", Just (boolQP snapWaitForCompletion)),
          ("master_timeout",) . Just . renderDuration <$> snapMasterTimeout
        ]
    body = encode $ object prs
    prs =
      catMaybes
        [ ("indices" .=) . indexSelectionName <$> snapIndices,
          Just ("ignore_unavailable" .= snapIgnoreUnavailable),
          Just ("include_global_state" .= snapIncludeGlobalState),
          Just ("partial" .= snapPartial),
          ("metadata" .=) <$> snapMetadata,
          ("feature_states" .=) . toList <$> snapFeatureStates
        ]
    renderDuration (u, n) = showText n <> timeUnitsSuffix u

indexSelectionName :: IndexSelection -> Text
indexSelectionName AllIndexes = "_all"
indexSelectionName (IndexList (i :| is)) = T.intercalate "," (unIndexName <$> (i : is))

-- | Clone a snapshot within the same repository, copying a (partial or
-- full) selection of indices from the source snapshot to a new target
-- snapshot. Pinned to 'StatusDependant' so that a 404 (missing
-- repository or source snapshot) and a 409 (target already exists)
-- surface as an 'EsError'. The wire format is identical across
-- Elasticsearch and OpenSearch, so this builder is shared by every
-- versioned client via "Database.Bloodhound.Common.Client".
--
-- The target snapshot name is carried in the URL path
-- (@PUT /_snapshot\/{repo}\/{snap}\/_clone\/{target}@) per the ES and
-- OpenSearch docs; the request body carries only the @indices@
-- selection.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/clone-snapshot-api.html>,
-- <https://docs.opensearch.org/latest/api-reference/snapshots/clone-snapshot/>)
cloneSnapshot ::
  SnapshotRepoName ->
  SnapshotName ->
  SnapshotName ->
  SnapshotCloneSettings ->
  BHRequest StatusDependant Acknowledged
cloneSnapshot
  (SnapshotRepoName repoName)
  (SnapshotName snapName)
  (SnapshotName targetName)
  SnapshotCloneSettings {..} =
    put endpoint (encode body)
    where
      endpoint =
        ["_snapshot", repoName, snapName, "_clone", targetName]
          `withQueries` params
      params =
        catMaybes
          [ ("master_timeout",) . Just . renderDuration <$> snapCloneMasterTimeout
          ]
      body =
        object $
          catMaybes
            [ ("indices" .=) . indexSelectionName <$> snapCloneIndices
            ]
      renderDuration (u, n) = showText n <> timeUnitsSuffix u

-- | Get info about known snapshots given a pattern and repo name.
getSnapshots :: SnapshotRepoName -> SnapshotSelection -> BHRequest StatusDependant [SnapshotInfo]
getSnapshots repoName sel = getSnapshotsWith repoName sel defaultSnapshotSelectionOptions

-- | Variant of 'getSnapshots' that accepts 'SnapshotSelectionOptions'
-- for the @master_timeout@, @verbose@, @index_details@,
-- @include_repository@, @sort@, @order@, @size@, @offset@, @after@,
-- @from_sort_value@, @ignore_unavailable@, @index_names@ and
-- @slm_policy_filter@ URI parameters. Calling with
-- 'defaultSnapshotSelectionOptions' is byte-for-byte identical to
-- 'getSnapshots'.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/get-snapshot-api.html>)
getSnapshotsWith ::
  SnapshotRepoName ->
  SnapshotSelection ->
  SnapshotSelectionOptions ->
  BHRequest StatusDependant [SnapshotInfo]
getSnapshotsWith (SnapshotRepoName repoName) sel opts =
  unSIs <$> get (["_snapshot", repoName, snapPath] `withQueries` snapshotSelectionOptionsParams opts)
  where
    snapPath = case sel of
      AllSnapshots -> "_all"
      SnapshotList (s :| ss) -> T.intercalate "," (renderPath <$> (s : ss))
    renderPath (SnapPattern t) = t
    renderPath (ExactSnap (SnapshotName t)) = t

-- | Render the 'SnapshotSelectionOptions' URI parameters
-- (@master_timeout@, @verbose@, @index_details@,
-- @include_repository@, @sort@, @order@, @size@, @offset@, @after@,
-- @from_sort_value@, @ignore_unavailable@, @index_names@,
-- @slm_policy_filter@) as a list of @(key, value)@ pairs suitable for
-- 'withQueries'. 'Nothing' fields are omitted, so
-- 'defaultSnapshotSelectionOptions' produces an empty list (and
-- therefore no query string).
snapshotSelectionOptionsParams :: SnapshotSelectionOptions -> [(Text, Maybe Text)]
snapshotSelectionOptionsParams opts =
  catMaybes
    [ ("master_timeout",) . Just . renderDuration <$> ssoMasterTimeout opts,
      ("verbose",) . Just . renderBool <$> ssoVerbose opts,
      ("index_details",) . Just . renderBool <$> ssoIndexDetails opts,
      ("include_repository",) . Just . renderBool <$> ssoIncludeRepository opts,
      ("sort",) . Just . renderSnapshotSortField <$> ssoSort opts,
      ("order",) . Just . renderSnapshotSortOrder <$> ssoOrder opts,
      ("size",) . Just . showText <$> ssoSize opts,
      ("offset",) . Just . showText <$> ssoOffset opts,
      ("after",) . Just <$> ssoAfter opts,
      ("from_sort_value",) . Just <$> ssoFromSortValue opts,
      ("ignore_unavailable",) . Just . renderBool <$> ssoIgnoreUnavailableSnapshots opts,
      ("index_names",) . Just . renderBool <$> ssoIndexNames opts,
      ("slm_policy_filter",) . Just <$> ssoSlmPolicyFilter opts
    ]
  where
    renderDuration (u, n) = showText n <> timeUnitsSuffix u
    renderBool True = "true"
    renderBool False = "false"

-- | Render the 'SnapshotMasterTimeoutOptions' URI parameter
-- (@master_timeout@) as a list of @(key, value)@ pairs suitable for
-- 'withQueries'. 'Nothing' fields are omitted, so
-- 'defaultSnapshotMasterTimeoutOptions' produces an empty list (and
-- therefore no query string).
snapshotMasterTimeoutOptionsParams :: SnapshotMasterTimeoutOptions -> [(Text, Maybe Text)]
snapshotMasterTimeoutOptionsParams opts =
  catMaybes
    [ ("master_timeout",) . Just . renderDuration <$> smtoMasterTimeout opts
    ]
  where
    renderDuration (u, n) = showText n <> timeUnitsSuffix u

-- | Render the 'SnapshotRepoTimeoutOptions' URI parameters
-- (@master_timeout@, @timeout@) as a list of @(key, value)@ pairs
-- suitable for 'withQueries'. 'Nothing' fields are omitted, so
-- 'defaultSnapshotRepoTimeoutOptions' produces an empty list (and
-- therefore no query string).
snapshotRepoTimeoutOptionsParams :: SnapshotRepoTimeoutOptions -> [(Text, Maybe Text)]
snapshotRepoTimeoutOptionsParams opts =
  catMaybes
    [ ("master_timeout",) . Just . renderDuration <$> srtoMasterTimeout opts,
      ("timeout",) . Just . renderDuration <$> srtoTimeout opts
    ]
  where
    renderDuration (u, n) = showText n <> timeUnitsSuffix u

-- | Render the 'SnapshotRepoGetOptions' URI parameters
-- (@master_timeout@, @local@) as a list of @(key, value)@ pairs
-- suitable for 'withQueries'. 'Nothing' fields are omitted, so
-- 'defaultSnapshotRepoGetOptions' produces an empty list (and
-- therefore no query string).
snapshotRepoGetOptionsParams :: SnapshotRepoGetOptions -> [(Text, Maybe Text)]
snapshotRepoGetOptionsParams opts =
  catMaybes
    [ ("local",) . Just . boolQP <$> srgoLocal opts,
      ("master_timeout",) . Just . renderDuration <$> srgoMasterTimeout opts
    ]
  where
    renderDuration (u, n) = showText n <> timeUnitsSuffix u

-- | Render the 'SnapshotStatusOptions' URI parameters
-- (@master_timeout@, @ignore_unavailable@) as a list of
-- @(key, value)@ pairs suitable for 'withQueries'. 'Nothing' fields
-- are omitted, so 'defaultSnapshotStatusOptions' produces an empty
-- list (and therefore no query string).
snapshotStatusOptionsParams :: SnapshotStatusOptions -> [(Text, Maybe Text)]
snapshotStatusOptionsParams opts =
  catMaybes
    [ ("ignore_unavailable",) . Just . boolQP <$> sstoIgnoreUnavailable opts,
      ("master_timeout",) . Just . renderDuration <$> sstoMasterTimeout opts
    ]
  where
    renderDuration (u, n) = showText n <> timeUnitsSuffix u

newtype SIs = SIs {unSIs :: [SnapshotInfo]}
  deriving newtype (Eq, Show)

instance FromJSON SIs where
  parseJSON = withObject "Collection of SnapshotInfo" parse
    where
      parse o = SIs <$> o .: "snapshots"

-- | Get detailed status of in-progress (or completed) snapshots. Use
-- this rather than 'getSnapshots' when you need shard-level progress
-- (files copied, sizes, stages) for monitoring non-blocking snapshot
-- workflows.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/get-snapshot-status-api.html>)
getSnapshotStatus ::
  SnapshotRepoName ->
  SnapshotName ->
  BHRequest StatusDependant [SnapshotStatus]
getSnapshotStatus repoName snapName = getSnapshotStatusWith repoName snapName defaultSnapshotStatusOptions

-- | Variant of 'getSnapshotStatus' that accepts
-- 'SnapshotStatusOptions' for the @master_timeout@ and
-- @ignore_unavailable@ URI parameters. Calling with
-- 'defaultSnapshotStatusOptions' is byte-for-byte identical to
-- 'getSnapshotStatus'.
getSnapshotStatusWith ::
  SnapshotRepoName ->
  SnapshotName ->
  SnapshotStatusOptions ->
  BHRequest StatusDependant [SnapshotStatus]
getSnapshotStatusWith (SnapshotRepoName repoName) (SnapshotName snapName) opts =
  unSSs <$> get (["_snapshot", repoName, snapName, "_status"] `withQueries` snapshotStatusOptionsParams opts)

newtype SSs = SSs {unSSs :: [SnapshotStatus]}
  deriving newtype (Eq, Show)

instance FromJSON SSs where
  parseJSON = withObject "Collection of SnapshotStatus" parse
    where
      parse o = SSs <$> o .: "snapshots"

-- | Delete a snapshot. Cancels if it is running.
deleteSnapshot :: SnapshotRepoName -> SnapshotName -> BHRequest StatusIndependant Acknowledged
deleteSnapshot repoName snapName = deleteSnapshotWith repoName snapName defaultSnapshotMasterTimeoutOptions

-- | Variant of 'deleteSnapshot' that accepts
-- 'SnapshotMasterTimeoutOptions' for the @master_timeout@ URI
-- parameter. Calling with 'defaultSnapshotMasterTimeoutOptions' is
-- byte-for-byte identical to 'deleteSnapshot'.
deleteSnapshotWith ::
  SnapshotRepoName ->
  SnapshotName ->
  SnapshotMasterTimeoutOptions ->
  BHRequest StatusIndependant Acknowledged
deleteSnapshotWith (SnapshotRepoName repoName) (SnapshotName snapName) opts =
  delete (["_snapshot", repoName, snapName] `withQueries` snapshotMasterTimeoutOptionsParams opts)

-- | Restore a snapshot to the cluster See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/modules-snapshots.html#_restore>
-- for more details.
restoreSnapshot ::
  SnapshotRepoName ->
  SnapshotName ->
  -- | Start with 'defaultSnapshotRestoreSettings' and customize
  -- from there for reasonable defaults.
  SnapshotRestoreSettings ->
  BHRequest StatusIndependant Accepted
restoreSnapshot (SnapshotRepoName repoName) (SnapshotName snapName) SnapshotRestoreSettings {..} =
  post endpoint (encode body)
  where
    endpoint = ["_snapshot", repoName, snapName, "_restore"] `withQueries` params
    params =
      catMaybes
        [ Just ("wait_for_completion", Just (boolQP snapRestoreWaitForCompletion)),
          ("master_timeout",) . Just . renderDuration <$> snapRestoreMasterTimeout
        ]
    body =
      object $
        catMaybes
          [ ("indices" .=) . indexSelectionName <$> snapRestoreIndices,
            Just ("ignore_unavailable" .= snapRestoreIgnoreUnavailable),
            Just ("include_global_state" .= snapRestoreIncludeGlobalState),
            ("rename_pattern" .=) <$> snapRestoreRenamePattern,
            ("rename_replacement" .=) . renderTokens <$> snapRestoreRenameReplacement,
            Just ("include_aliases" .= snapRestoreIncludeAliases),
            ("index_settings" .=) <$> snapRestoreIndexSettingsOverrides,
            ("ignore_index_settings" .=) <$> snapRestoreIgnoreIndexSettings
          ]
    renderTokens (t :| ts) = mconcat (renderToken <$> (t : ts))
    renderToken (RRTLit t) = t
    renderToken RRSubWholeMatch = "$0"
    renderToken (RRSubGroup g) = T.pack (show (rrGroupRefNum g))
    renderDuration (u, n) = showText n <> timeUnitsSuffix u

getNodesInfo :: NodeSelection -> BHRequest StatusDependant NodesInfo
getNodesInfo sel = getNodesInfoWith sel defaultNodeInfoOptions

-- | 'getNodesInfoWith' is the fully-parameterised form of
-- 'getNodesInfo'. 'NodeInfoOptions' carries the @/{metrics}@ path
-- segment and the @flat_settings@, @master_timeout@, and @timeout@
-- query parameters.
--
-- Passing 'defaultNodeInfoOptions' yields a request byte-for-byte
-- identical to 'getNodesInfo'.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-nodes-info.html>)
getNodesInfoWith ::
  NodeSelection ->
  NodeInfoOptions ->
  BHRequest StatusDependant NodesInfo
getNodesInfoWith sel opts =
  get $ mkEndpoint endpointParts `withQueries` queries
  where
    endpointParts =
      ["_nodes", selectionSeg]
        <> case nioMetrics opts of
          Just ms
            | not (null ms) ->
                ["info", T.intercalate "," (renderNodeInfoMetric <$> ms)]
          _ -> []
    selectionSeg = nodeSelectionSeg sel
    queries =
      catMaybes
        [ ("flat_settings",) . Just . renderBool <$> nioFlatSettings opts,
          ("master_timeout",) . Just . renderDuration <$> nioMasterTimeout opts,
          ("timeout",) . Just . renderDuration <$> nioTimeout opts
        ]
    renderDuration (u, n) = showText n <> timeUnitsSuffix u
    renderBool True = "true"
    renderBool False = "false"

getNodesStats :: NodeSelection -> BHRequest StatusDependant NodesStats
getNodesStats sel = getNodesStatsWith sel defaultNodeStatsOptions

-- | 'getNodesStatsWith' is the fully-parameterised form of
-- 'getNodesStats'. 'NodeStatsOptions' carries the @/{metrics}@ path
-- segment and the documented query parameters (@completion_fields@,
-- @fielddata_fields@, @fields@, @groups@, @level@, @types@,
-- @master_timeout@, @timeout@, @include_segment_file_sizes@,
-- @include_unloaded_segments@).
--
-- Passing 'defaultNodeStatsOptions' yields a request byte-for-byte
-- identical to 'getNodesStats'.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-nodes-stats.html>)
getNodesStatsWith ::
  NodeSelection ->
  NodeStatsOptions ->
  BHRequest StatusDependant NodesStats
getNodesStatsWith sel opts =
  get $ mkEndpoint endpointParts `withQueries` queries
  where
    endpointParts =
      ["_nodes", selectionSeg, "stats"]
        <> case nsoMetrics opts of
          Just ms
            | not (null ms) ->
                [T.intercalate "," (renderNodeStatsMetric <$> ms)]
          _ -> []
    selectionSeg = nodeSelectionSeg sel
    queries =
      catMaybes
        [ ("completion_fields",) . Just <$> nsoCompletionFields opts,
          ("fielddata_fields",) . Just <$> nsoFielddataFields opts,
          ("fields",) . Just <$> nsoFields opts,
          ("groups",) . Just <$> nsoGroups opts,
          ("level",) . Just . renderNodeStatsLevel <$> nsoLevel opts,
          ("types",) . Just <$> nsoTypes opts,
          ("master_timeout",) . Just . renderDuration <$> nsoMasterTimeout opts,
          ("timeout",) . Just . renderDuration <$> nsoTimeout opts,
          ("include_segment_file_sizes",) . Just . renderBool <$> nsoIncludeSegmentFileSizes opts,
          ("include_unloaded_segments",) . Just . renderBool <$> nsoIncludeUnloadedSegments opts
        ]
    renderDuration (u, n) = showText n <> timeUnitsSuffix u
    renderBool True = "true"
    renderBool False = "false"

getNodesUsage :: NodeSelection -> BHRequest StatusDependant NodesUsage
getNodesUsage sel = getNodesUsageWith sel defaultNodeUsageOptions

-- | 'getNodesUsageWith' is the fully-parameterised form of
-- 'getNodesUsage'. 'NodeUsageOptions' carries the @/{metrics}@ path
-- segment and the @master_timeout@ and @timeout@ query parameters.
--
-- Passing 'defaultNodeUsageOptions' yields a request byte-for-byte
-- identical to 'getNodesUsage'.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-nodes-usage.html>)
getNodesUsageWith ::
  NodeSelection ->
  NodeUsageOptions ->
  BHRequest StatusDependant NodesUsage
getNodesUsageWith sel opts =
  get $ mkEndpoint endpointParts `withQueries` queries
  where
    endpointParts =
      ["_nodes", selectionSeg, "usage"]
        <> case nuoMetrics opts of
          Just ms
            | not (null ms) ->
                [T.intercalate "," (renderNodeUsageMetric <$> ms)]
          _ -> []
    selectionSeg = nodeSelectionSeg sel
    queries =
      catMaybes
        [ ("master_timeout",) . Just . renderDuration <$> nuoMasterTimeout opts,
          ("timeout",) . Just . renderDuration <$> nuoTimeout opts
        ]
    renderDuration (u, n) = showText n <> timeUnitsSuffix u

-- | 'nodeSelectionSeg' renders a 'NodeSelection' as the single path
-- segment ES\/OS use to identify nodes in @_nodes@, @_cluster@, and
-- related endpoints: @_local@ for 'LocalNode', @_all@ for 'AllNodes',
-- or a comma-joined list of selector renderings for 'NodeList'.
nodeSelectionSeg :: NodeSelection -> Text
nodeSelectionSeg sel = case sel of
  LocalNode -> "_local"
  NodeList (l :| ls) -> T.intercalate "," (nodeSelectorSeg <$> (l : ls))
  AllNodes -> "_all"

-- | 'nodeSelectorSeg' renders a single 'NodeSelector' as it appears in
-- a comma-joined 'NodeList' path segment. Pattern matches the
-- individual 'NodeSelector' forms (by name, full id, host, or
-- attribute).
nodeSelectorSeg :: NodeSelector -> Text
nodeSelectorSeg (NodeByName (NodeName n)) = n
nodeSelectorSeg (NodeByFullNodeId (FullNodeId i)) = i
nodeSelectorSeg (NodeByHost (Server s)) = s
nodeSelectorSeg (NodeByAttribute (NodeAttrName a) v) = a <> ":" <> v

-- | 'getNodesHotThreads' fetches the @GET /_nodes/hot_threads@ report
-- (the first diagnostic tool for high CPU). The response is plain text,
-- not JSON, so the body is returned verbatim as 'Text'.
--
-- This is the legacy two-parameter form: it exposes only the 'NodeSelector'
-- and the @threads@ URI parameter. It delegates to
-- 'getNodesHotThreadsWith' with 'defaultHotThreadsOptions', so the wire
-- format is unchanged.
--
-- * @Nothing@ selector → @GET /_nodes/hot_threads@ (server picks a node).
-- * @Just sel@ → @GET /_nodes/{seg}/hot_threads@ restricted to that node.
-- * @Just n@ thread count → @?threads=n@ query parameter.
--
-- To set the other documented URI parameters (@interval@, @snapshots@,
-- @type@, @ignore_idle_threads@, @master_timeout@, @timeout@), use
-- 'getNodesHotThreadsWith'.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-nodes-hot-threads.html>)
getNodesHotThreads ::
  Maybe NodeSelector ->
  Maybe ThreadCount ->
  BHRequest StatusDependant Text
getNodesHotThreads mSel mThreadCount =
  getNodesHotThreadsWith mSel defaultHotThreadsOptions {htoThreads = mThreadCount}

-- | 'getNodesHotThreadsWith' is the fully-parameterised form of
-- 'getNodesHotThreads'. Every URI parameter accepted by
-- @GET /_nodes/hot_threads@ is exposed via 'HotThreadsOptions'; pass
-- 'defaultHotThreadsOptions' to reproduce the legacy behaviour (only the
-- selector, no query string). See 'hotThreadsOptionsParams' for the
-- rendering of each field.
--
-- * @Nothing@ selector → @GET /_nodes/hot_threads@ (server picks a node).
-- * @Just sel@ → @GET /_nodes/{seg}/hot_threads@ restricted to that node.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-nodes-hot-threads.html>)
getNodesHotThreadsWith ::
  Maybe NodeSelector ->
  HotThreadsOptions ->
  BHRequest StatusDependant Text
getNodesHotThreadsWith mSel opts =
  getText $ endpoint `withQueries` hotThreadsOptionsParams opts
  where
    endpoint = case mSel of
      Nothing -> ["_nodes", "hot_threads"]
      Just sel -> ["_nodes", nodeSelectorSeg sel, "hot_threads"]

-- | Render the 'HotThreadsOptions' URI parameters
-- (@threads@, @interval@, @snapshots@, @type@,
-- @ignore_idle_threads@, @master_timeout@, @timeout@) as a list of
-- @(key, value)@ pairs suitable for 'withQueries'. 'Nothing' fields are
-- omitted, so 'defaultHotThreadsOptions' produces an empty list (and
-- therefore no query string).
--
-- The hot-threads kind ('htoDocType') is emitted under the @type@ key,
-- which is the name accepted by every supported backend (ES and
-- OpenSearch). The historical name @doc_type@ is not recognised by the
-- server and is deliberately not emitted.
hotThreadsOptionsParams :: HotThreadsOptions -> [(Text, Maybe Text)]
hotThreadsOptionsParams opts =
  catMaybes
    [ ("threads",) . Just . showText . threadCount <$> htoThreads opts,
      ("interval",) . Just . renderDuration <$> htoInterval opts,
      ("snapshots",) . Just . showText <$> htoSnapshots opts,
      ("type",) . Just . renderHotThreadsDocType <$> htoDocType opts,
      ("ignore_idle_threads",) . Just . renderBool <$> htoIgnoreIdleThreads opts,
      ("master_timeout",) . Just . renderDuration <$> htoMasterTimeout opts,
      ("timeout",) . Just . renderDuration <$> htoTimeout opts
    ]
  where
    renderDuration (u, n) = showText n <> timeUnitsSuffix u
    renderBool True = "true"
    renderBool False = "false"

-- | 'reloadSecureSettings' reloads keystore-backed secure settings on
-- the selected nodes without a restart. Maps to
-- @POST /_nodes/reload_secure_settings@ (cluster-wide when @Nothing@)
-- or @POST /_nodes/{seg}/reload_secure_settings@ scoped to a
-- 'NodeSelection' when @Just@. Returns 'Acknowledged' on success.
--
-- * @Nothing@              → @POST /_nodes/reload_secure_settings@
-- * @Just 'LocalNode'@     → @POST /_nodes/_local/reload_secure_settings@
-- * @Just 'AllNodes'@      → @POST /_nodes/_all/reload_secure_settings@
-- * @Just ('NodeList' ...)@ → @POST /_nodes/{csv}/reload_secure_settings@
--
-- The request always carries an empty body. ES optionally accepts a
-- @secure_settings@ object for keystore re-encryption; that variant is
-- not exposed here. Callers who need it should file a follow-up.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-nodes-reload-secure.html>
-- (or the OpenSearch equivalent).
reloadSecureSettings ::
  Maybe NodeSelection ->
  BHRequest StatusDependant Acknowledged
reloadSecureSettings mSel =
  post endpoint emptyBody
  where
    endpoint = case mSel of
      Nothing -> ["_nodes", "reload_secure_settings"]
      Just sel -> ["_nodes", nodeSelectionSeg sel, "reload_secure_settings"]

getClusterHealth :: BHRequest StatusDependant ClusterHealth
getClusterHealth = getClusterHealthWith defaultClusterHealthOptions

-- | 'getClusterHealthWith' is the fully-parameterised form of
-- 'getClusterHealth'. Every URI parameter accepted by
-- @GET /_cluster/health@ is exposed via 'ClusterHealthOptions'; pass
-- 'defaultClusterHealthOptions' to reproduce the legacy behaviour
-- (no parameters). See 'clusterHealthOptionsParams' for the rendering of
-- each field.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-health.html>)
getClusterHealthWith :: ClusterHealthOptions -> BHRequest StatusDependant ClusterHealth
getClusterHealthWith opts =
  get $ ["_cluster", "health"] `withQueries` clusterHealthOptionsParams opts

-- | 'getClusterHealthForIndex' fetches the cluster health restricted to a
-- single index — the @GET /_cluster/health/{index}@ form. It is the
-- per-index counterpart to 'getClusterHealth'.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-health.html>)
getClusterHealthForIndex :: IndexName -> BHRequest StatusDependant ClusterHealth
getClusterHealthForIndex = getClusterHealthForIndexWith defaultClusterHealthOptions

-- | 'getClusterHealthForIndexWith' combines the per-index form with the
-- full 'ClusterHealthOptions' parameter set (e.g.
-- @wait_for_status@, @timeout@, @level@). It shares the
-- 'clusterHealthOptionsParams' renderer with 'waitForYellowIndex' but
-- returns the structured 'ClusterHealth' response rather than 'HealthStatus'.
--
-- /Note/: this builder is 'StatusDependant'. When @choWaitForStatus@ is
-- set and the index cannot reach that status within @choTimeout@, the
-- server responds with HTTP 503 and this function surfaces it as an
-- 'EsError'. Use 'waitForYellowIndex' if you want the 503 absorbed into
-- the returned 'HealthStatus' instead.
getClusterHealthForIndexWith ::
  ClusterHealthOptions ->
  IndexName ->
  BHRequest StatusDependant ClusterHealth
getClusterHealthForIndexWith opts indexName =
  get $ ["_cluster", "health", unIndexName indexName] `withQueries` clusterHealthOptionsParams opts

-- | Render the 'ClusterHealthOptions' URI parameters
-- (@level@, @local@, @master_timeout@, @wait_for_status@,
-- @wait_for_active_shards@, @wait_for_nodes@,
-- @wait_for_no_relocating_shards@, @wait_for_no_initializing_shards@,
-- @timeout@) as a list of @(key, value)@ pairs suitable for 'withQueries'.
-- 'Nothing' fields are omitted, so 'defaultClusterHealthOptions' produces
-- an empty list (and therefore no query string).
clusterHealthOptionsParams :: ClusterHealthOptions -> [(Text, Maybe Text)]
clusterHealthOptionsParams opts =
  catMaybes
    [ ("level",) . Just . renderClusterHealthLevel <$> choLevel opts,
      ("local",) . Just . renderBool <$> choLocal opts,
      ("master_timeout",) . Just . renderDuration <$> choMasterTimeout opts,
      ("wait_for_status",) . Just . renderClusterHealthStatus <$> choWaitForStatus opts,
      ("wait_for_active_shards",) . Just . renderActiveShardCount <$> choWaitForActiveShards opts,
      ("wait_for_nodes",) . Just <$> choWaitForNodes opts,
      ("wait_for_no_relocating_shards",) . Just . renderBool <$> choWaitForNoRelocatingShards opts,
      ("wait_for_no_initializing_shards",) . Just . renderBool <$> choWaitForNoInitializingShards opts,
      ("timeout",) . Just . renderDuration <$> choTimeout opts
    ]
  where
    renderDuration (u, n) = showText n <> timeUnitsSuffix u
    renderBool True = "true"
    renderBool False = "false"
    renderClusterHealthStatus ClusterHealthGreen = "green"
    renderClusterHealthStatus ClusterHealthYellow = "yellow"
    renderClusterHealthStatus ClusterHealthRed = "red"

-- | 'getClusterState' fetches the full cluster state — the
-- @GET /_cluster/state@ endpoint. Equivalent to
-- @'getClusterStateWith' 'defaultClusterStateOptions'@ (no path
-- filters, no query parameters, server returns everything). Use
-- 'getClusterStateWith' to pass metric\/index filters or
-- @local@\/@master_timeout@\/@wait_for_metadata_version@\/@filter_path@.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-state.html>)
getClusterState :: BHRequest StatusDependant ClusterState
getClusterState = getClusterStateWith defaultClusterStateOptions

-- | 'getClusterStateWith' is the fully-parameterised form of
-- 'getClusterState'. Every URI shape accepted by
-- @GET /_cluster/state@ is exposed via 'ClusterStateOptions':
--
--  * @cstoMetrics@ selects which top-level sections come back
--    (@/_cluster/state/{metric}@); 'Nothing' renders as @_all@.
--  * @cstoIndices@ narrows the @metadata@ and @routing_table@ sections
--    to a comma-separated index expression
--    (@/_cluster/state/{metric}/{index}@).
--  * @cstoLocal@, @cstoMasterTimeout@, @cstoWaitForMetadataVersion@
--    and @cstoFilterPath@ are forwarded as query parameters.
--
-- Pass 'defaultClusterStateOptions' to reproduce the bare behaviour
-- (no path segments beyond @/_cluster/state@, no query string).
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-state.html>)
getClusterStateWith ::
  ClusterStateOptions ->
  BHRequest StatusDependant ClusterState
getClusterStateWith opts =
  get $
    mkEndpoint (["_cluster", "state"] ++ clusterStateOptionsPathSegments opts)
      `withQueries` clusterStateOptionsParams opts

-- | 'getClusterStats' returns cluster-level statistics — the
-- @GET /_cluster/stats@ endpoint. The response rolls up indices counts,
-- shards, docs, store size, versions and per-node aggregates (count by
-- role, JVM, FS, OS, plugins, ingest, ...). Only the universally-useful
-- fields are typed; the rest is preserved verbatim in the @*Other@
-- 'Value' fields of 'ClusterStatsIndices' and 'ClusterStatsNodes'.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-stats.html>)
getClusterStats :: BHRequest StatusDependant ClusterStats
getClusterStats = get ["_cluster", "stats"]

-- | 'getPendingTasks' returns the cluster-level changes not yet
-- executed by the master — the @GET /_cluster/pending_tasks@ endpoint.
-- These are pending index creations, mapping updates, shard starts,
-- reroutes and the like. The endpoint wraps its payload in a
-- @tasks@ array; 'getPendingTasks' unwraps it so callers receive the
-- plain list of 'PendingTask' values (empty when the cluster is idle).
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-pending.html>)
getPendingTasks :: BHRequest StatusDependant [PendingTask]
getPendingTasks = unPendingTasksResponse <$> get ["_cluster", "pending_tasks"]

-- | Wrapper used to decode the @{"tasks": [...]}@ envelope of
-- 'getPendingTasks' into the bare @['PendingTask']@ it carries. Mirrors
-- the @ListedIndexName@ trick used by 'listIndices'.
newtype PendingTasksResponse = PendingTasksResponse {unPendingTasksResponse :: [PendingTask]}
  deriving stock (Eq, Show)

instance FromJSON PendingTasksResponse where
  parseJSON = withObject "PendingTasksResponse" $ \o ->
    PendingTasksResponse <$> o .:? "tasks" .!= []

-- | 'explainAllocation' explains why a shard is (or is not) allocated —
-- the @/_cluster/allocation/explain@ endpoint. Pass 'Nothing' to ask
-- the server to explain the first unassigned shard it finds (sent as a
-- bodyless @GET@ — the server treats the absence of body as @{}@);
-- pass @'Just' req@ to target a specific shard (a body containing
-- @index@\/@shard@\/@primary@, sent as @POST@).
--
-- Per the ES contract, when any of 'aerIndex', 'aerShard' or
-- 'aerPrimary' is supplied all three must be supplied together — use
-- 'mkAllocationExplainRequest' to construct a valid body for the common
-- case.
--
-- The structured top-level fields of the response are typed
-- ('aeIndex', 'aeShard', 'aePrimary', 'aeCurrentState', ...); the
-- larger nested blobs ('aeDecisions', 'aeClusterInfo', 'aeNodes',
-- 'aeUnassignedInfo', 'aeCurrentNode') are kept as 'Data.Aeson.Value'
-- for inspection with aeson combinators.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-allocation-explain.html>)
explainAllocation ::
  Maybe AllocationExplainRequest ->
  BHRequest StatusDependant AllocationExplanation
explainAllocation Nothing = get ["_cluster", "allocation", "explain"]
explainAllocation (Just body) =
  post ["_cluster", "allocation", "explain"] (encode body)

-- | 'getRemoteClusterInfo' fetches connection state for every
-- configured remote cluster — the @GET /_remote/info@ endpoint. The
-- response is keyed by remote-cluster alias; a cluster with no remotes
-- configured returns an empty 'RemoteClustersInfo' (i.e. @{}@ on the
-- wire).
--
-- Note: this endpoint is Elasticsearch-only — OpenSearch does not
-- surface remote-cluster info at this path. Calls against an
-- OpenSearch cluster will return HTTP 404.
--
-- The @/_remote/info@ path is the documented URL on every supported
-- Elasticsearch version (7.x, 8.x, 9.x); no @_cluster@ prefix is
-- involved.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-remote-info.html>)
getRemoteClusterInfo :: BHRequest StatusDependant RemoteClustersInfo
getRemoteClusterInfo = get ["_remote", "info"]

-- | 'updateVotingConfigExclusions' is the @POST
-- /_cluster/voting_config_exclusions@ endpoint — manually adds
-- voting config exclusions for nodes that have left the cluster
-- permanently, so ES can shrink the voting configuration without
-- waiting for the auto-converge timeout. Pass the 'FullNodeId's
-- and\/or 'NodeName's of the departed nodes via
-- 'VotingConfigExclusionOptions'; the server requires at least one
-- entry across the two lists, otherwise it rejects the request with
-- @validation_exception@. Returns 'Acknowledged'.
--
-- Note: this endpoint is Elasticsearch-only — OpenSearch does not
-- expose voting config exclusions in its cluster API surface. Calls
-- against an OpenSearch cluster will return HTTP 404.
--
-- /Wire-shape note/: against live ES 7.17 this endpoint returns
-- HTTP 200 with an /empty/ body (Content-Length: 0), not the
-- @{\"acknowledged\":true}@ envelope 'Acknowledged' nominally
-- expects. 'parseEsResponse' handles this by normalising such an
-- empty body to the JSON value @null@ before invoking the
-- 'Acknowledged' 'FromJSON' instance, which maps @null@ to
-- 'Acknowledged' @True@. The pure endpoint-shape tests in
-- @Test.VotingConfigExclusionsSpec@ cover the request side; the
-- live round-trip covers the response-decoding canary, and
-- @Test.ParseEsResponseSpec@ pins the 'Acknowledged' @null@→@True@
-- mapping.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-post-voting-config-exclusions.html>)
updateVotingConfigExclusions ::
  VotingConfigExclusionOptions ->
  BHRequest StatusDependant Acknowledged
updateVotingConfigExclusions opts =
  post
    (["_cluster", "voting_config_exclusions"] `withQueries` votingConfigExclusionOptionsParams opts)
    emptyBody

-- | 'clearVotingConfigExclusions' is the @DELETE
-- /_cluster/voting_config_exclusions@ endpoint — clears all
-- voting config exclusions the cluster is currently holding. Pair
-- with 'updateVotingConfigExclusions' once the departed nodes have
-- been removed from the cluster topology (otherwise the exclusions
-- would block them rejoining). Returns 'Acknowledged'.
--
-- Note: this endpoint is Elasticsearch-only — OpenSearch does not
-- expose voting config exclusions in its cluster API surface. Calls
-- against an OpenSearch cluster will return HTTP 404.
--
-- /Wire-shape note/: same as 'updateVotingConfigExclusions' —
-- live ES 7.17 returns 200 with an empty body, decoded to
-- 'Acknowledged' @True@ via 'parseEsResponse' (empty body →
-- @null@ → 'Acknowledged' @True@).
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-delete-voting-config-exclusions.html>)
clearVotingConfigExclusions :: BHRequest StatusDependant Acknowledged
clearVotingConfigExclusions = delete ["_cluster", "voting_config_exclusions"]

-- | 'getClusterSettings' fetches cluster-wide settings — the
-- @GET /_cluster/settings@ endpoint. Returns the persistent and
-- transient settings layers; the defaults layer is omitted unless the
-- 'csoIncludeDefaults' option is set. Use 'getClusterSettingsWith' to
-- pass URI parameters.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-get-settings.html>)
getClusterSettings :: BHRequest StatusDependant ClusterSettings
getClusterSettings = getClusterSettingsWith defaultClusterSettingsOptions

-- | 'getClusterSettingsWith' is the fully-parameterised form of
-- 'getClusterSettings'. Every URI parameter accepted by
-- @GET /_cluster/settings@ is exposed via 'ClusterSettingsOptions';
-- pass 'defaultClusterSettingsOptions' to reproduce the legacy
-- behaviour (no parameters). See 'clusterSettingsOptionsParams' for the
-- rendering of each field.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-get-settings.html>)
getClusterSettingsWith :: ClusterSettingsOptions -> BHRequest StatusDependant ClusterSettings
getClusterSettingsWith opts =
  get $ ["_cluster", "settings"] `withQueries` clusterSettingsOptionsParams opts

-- | 'updateClusterSettings' is the write-side counterpart of
-- 'getClusterSettings' — the @PUT /_cluster/settings@ endpoint. It
-- applies the persistent and\/or transient settings carried by the
-- 'ClusterSettingsUpdate' body; assign 'Null' to a key to clear it.
-- Equivalent to @'updateClusterSettingsWith'
-- 'defaultClusterSettingsUpdateOptions'@ (no URI parameters).
--
-- /Note/: this builder is 'StatusDependant'. A malformed or rejected
-- setting surfaces as an 'EsError' rather than being swallowed.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-update-settings.html>)
updateClusterSettings ::
  ClusterSettingsUpdate ->
  BHRequest StatusDependant Acknowledged
updateClusterSettings = updateClusterSettingsWith defaultClusterSettingsUpdateOptions

-- | 'updateClusterSettingsWith' is the fully-parameterised form of
-- 'updateClusterSettings'. Every URI parameter accepted by
-- @PUT /_cluster/settings@ is exposed via 'ClusterSettingsUpdateOptions'
-- (@flat_settings@, @master_timeout@); pass
-- 'defaultClusterSettingsUpdateOptions' to reproduce the bare behaviour
-- (no parameters). See 'clusterSettingsUpdateOptionsParams' for the
-- rendering of each field.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-update-settings.html>)
updateClusterSettingsWith ::
  ClusterSettingsUpdateOptions ->
  ClusterSettingsUpdate ->
  BHRequest StatusDependant Acknowledged
updateClusterSettingsWith opts update =
  put endpoint (encode update)
  where
    endpoint =
      ["_cluster", "settings"]
        `withQueries` clusterSettingsUpdateOptionsParams opts

-- | 'rerouteCluster' manually executes shard reroute commands against
-- the cluster via @POST /_cluster/reroute@. Each 'RerouteCommand'
-- (move, cancel, allocate_replica, ...) is carried in the request body's
-- @commands@ array; the operation takes effect immediately. Returns
-- 'Acknowledged' on success.
--
-- For the non-mutating @dry_run\@true@ form — which instead returns the
-- resulting cluster state without applying anything — use
-- 'rerouteClusterWith' with 'roDryRun' set; its 'RerouteResponse'
-- surfaces the simulated state in 'rrState'.
--
-- /Note/: this builder is 'StatusDependant'. A rejected command surfaces
-- as an 'EsError' rather than being swallowed.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-reroute.html>)
rerouteCluster ::
  [RerouteCommand] ->
  BHRequest StatusDependant Acknowledged
rerouteCluster cmds = post ["_cluster", "reroute"] (encode body)
  where
    body = object ["commands" .= cmds]

-- | 'rerouteClusterWith' is the fully-parameterised form of
-- 'rerouteCluster'. Every URI parameter accepted by
-- @POST /_cluster/reroute@ is exposed via 'RerouteOptions'
-- (@dry_run@, @explain@, @metric@, @master_timeout@, @timeout@); pass
-- 'defaultRerouteOptions' to reproduce the bare behaviour. The response
-- is the rich 'RerouteResponse', whose 'rrState' is populated when
-- @dry_run\@true@ (or @explain@) and whose 'rrExplanations' is populated
-- when @explain\@true@. See 'rerouteOptionsParams' for the rendering of
-- each field.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cluster-reroute.html>)
rerouteClusterWith ::
  RerouteOptions ->
  [RerouteCommand] ->
  BHRequest StatusDependant RerouteResponse
rerouteClusterWith opts cmds = post endpoint (encode body)
  where
    body = object ["commands" .= cmds]
    endpoint = ["_cluster", "reroute"] `withQueries` rerouteOptionsParams opts

createIndex :: IndexSettings -> IndexName -> BHRequest StatusDependant Acknowledged
createIndex indexSettings indexName =
  put [unIndexName indexName] $ encode indexSettings

createIndexWith ::
  [UpdatableIndexSetting] ->
  -- | shard count
  Int ->
  IndexName ->
  BHRequest StatusIndependant Acknowledged
createIndexWith updates shards indexName =
  put [unIndexName indexName] body
  where
    body =
      encode $
        object
          [ "settings"
              .= deepMerge
                ( X.singleton "index.number_of_shards" (toJSON shards)
                    : [u | Object u <- toJSON <$> updates]
                )
          ]

-- | Create an index using the full 'CreateIndexOptions' record, exposing
-- every body field (@settings@, @mappings@, @aliases@) and URI parameter
-- (@wait_for_active_shards@, @master_timeout@, @timeout@) accepted by
-- @PUT \/\<index\>@
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-create-index.html>).
-- This is a superset of 'createIndex' \/ 'createIndexWith'.
--
-- Passing @'defaultCreateIndexOptions' { 'cioSettings' = 'Just' s }@ is
-- byte-for-byte identical to @'createIndex' s@ for the body, and adds no
-- URI parameters.
createIndexOptions ::
  CreateIndexOptions ->
  IndexName ->
  BHRequest StatusDependant Acknowledged
createIndexOptions opts indexName =
  put endpoint body
  where
    endpoint =
      [unIndexName indexName]
        `withQueries` createIndexOptionsParams opts
    body = fromMaybe emptyBody (createIndexOptionsBody opts)

-- | Render the 'CreateIndexOptions' body fields (@settings@, @mappings@,
-- @aliases@) as a single JSON object, returning 'Nothing' when all three
-- are absent (the caller then sends an empty body and lets the server
-- apply its defaults).
createIndexOptionsBody :: CreateIndexOptions -> Maybe L.ByteString
createIndexOptionsBody opts =
  case (cioSettings opts, cioMappings opts, cioAliases opts) of
    (Nothing, Nothing, Nothing) -> Nothing
    _ -> Just . encode . Object $ deepMerge objects
  where
    objects :: [X.KeyMap Value]
    objects =
      catMaybes
        [ jsonObject <$> cioSettings opts,
          X.singleton "mappings" <$> cioMappings opts,
          X.singleton "aliases" . Object <$> cioAliases opts
        ]

-- | Render the 'CreateIndexOptions' URI parameters
-- (@wait_for_active_shards@, @master_timeout@, @timeout@) as a list of
-- @(key, value)@ pairs suitable for 'withQueries'. 'Nothing' fields are
-- omitted, so 'defaultCreateIndexOptions' produces an empty list.
createIndexOptionsParams :: CreateIndexOptions -> [(Text, Maybe Text)]
createIndexOptionsParams opts =
  catMaybes
    [ ("wait_for_active_shards",) . Just . renderActiveShardCount <$> cioWaitForActiveShards opts,
      ("master_timeout",) . Just . renderDuration <$> cioMasterTimeout opts,
      ("timeout",) . Just . renderDuration <$> cioTimeout opts
    ]
  where
    renderDuration (u, n) = showText n <> timeUnitsSuffix u

-- | 'flushIndex' will flush an index given a 'Server' and an 'IndexName'.
--
-- Equivalent to @'flushIndexWith' 'defaultFlushIndexOptions'@. Use the
-- @With@ variant to pass @wait_if_ongoing@, @force@, @ignore_unavailable@,
-- @allow_no_indices@ or @expand_wildcards@.
--
-- Returns 'ShardsResult' (the @{\"_shards\": {...}}@ envelope) because
-- ES\/OS wraps the shard stats in a top-level @_shards@ key. The inner
-- 'ShardResult' is reachable via 'srShards'.
flushIndex :: IndexName -> BHRequest StatusDependant ShardsResult
flushIndex = flushIndexWith defaultFlushIndexOptions

-- | 'flushIndexWith' is the fully-parameterised form of 'flushIndex'.
-- Every URI parameter accepted by @POST /<index>/_flush@ is exposed via
-- 'FlushIndexOptions'; pass 'defaultFlushIndexOptions' to reproduce the
-- legacy behaviour (no parameters).
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-flush.html>)
flushIndexWith ::
  FlushIndexOptions ->
  IndexName ->
  BHRequest StatusDependant ShardsResult
flushIndexWith opts indexName =
  post endpoint emptyBody
  where
    endpoint =
      [unIndexName indexName, "_flush"]
        `withQueries` flushIndexOptionsParams opts

-- | 'clearIndexCache' clears the caches (query, fielddata, request)
-- for a single index. Wraps @POST \/\<index\>\/_cache\/clear@.
--
-- Returns 'ShardsResult' (the @{\"_shards\": {...}}@ envelope), like the
-- neighbouring 'flushIndex' and 'refreshIndex', because the ES\/OS
-- response wraps the shard stats in a top-level @_shards@ key. Only
-- @?fielddata@, @?query@, @?request@ for selective clearing are not yet
-- exposed.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-clearcache.html>
-- and
-- <https://docs.opensearch.org/latest/api-reference/index-apis/clear-cache/>.
--
-- @since 0.26.0.0
clearIndexCache :: IndexName -> BHRequest StatusDependant ShardsResult
clearIndexCache indexName =
  post [unIndexName indexName, "_cache", "clear"] emptyBody

-- | 'reloadSearchAnalyzers' reloads an index's updateable search
-- analyzers (picking up changes to synonym files used by
-- @updateable@ @synonym@\/@synonym_graph@ filters). Maps to
-- @POST \/{index}/_reload_search_analyzers@ and returns a
-- 'ReloadSearchAnalyzersResponse'.
--
-- Equivalent to
-- @'reloadSearchAnalyzersWith' 'defaultReloadSearchAnalyzersOptions'@.
-- Use the @With@ variant to pass @ignore_unavailable@,
-- @allow_no_indices@ or @expand_wildcards@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-reload-analyzers.html>
-- and
-- <https://docs.opensearch.org/latest/api-reference/index-apis/reload-search-analyzers/>.
--
-- @since 0.26.0.0
reloadSearchAnalyzers ::
  IndexName ->
  BHRequest StatusDependant ReloadSearchAnalyzersResponse
reloadSearchAnalyzers = reloadSearchAnalyzersWith defaultReloadSearchAnalyzersOptions

-- | 'reloadSearchAnalyzersWith' is the fully-parameterised form of
-- 'reloadSearchAnalyzers'. Every URI parameter accepted by
-- @POST \/{index}/_reload_search_analyzers@ is exposed via
-- 'ReloadSearchAnalyzersOptions'; pass
-- 'defaultReloadSearchAnalyzersOptions' to reproduce the
-- parameterless behaviour.
reloadSearchAnalyzersWith ::
  ReloadSearchAnalyzersOptions ->
  IndexName ->
  BHRequest StatusDependant ReloadSearchAnalyzersResponse
reloadSearchAnalyzersWith opts indexName =
  post endpoint emptyBody
  where
    endpoint =
      [unIndexName indexName, "_reload_search_analyzers"]
        `withQueries` reloadSearchAnalyzersOptionsParams opts

-- | 'diskUsage' analyses the disk usage of each field of an index (or
-- the latest backing index of a data stream). Maps to
-- @POST \/{index}/_disk_usage@ and returns a 'DiskUsageResponse'.
--
-- This is an ES-only feature (core since 7.15, technical preview) and a
-- resource-intensive operation; the API requires
-- @run_expensive_tasks=true@, which 'defaultDiskUsageOptions' sets for
-- you. Equivalent to @'diskUsageWith' 'defaultDiskUsageOptions'@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-disk-usage.html>.
--
-- @since 0.26.0.0
diskUsage ::
  IndexName ->
  BHRequest StatusDependant DiskUsageResponse
diskUsage = diskUsageWith defaultDiskUsageOptions

-- | 'diskUsageWith' is the fully-parameterised form of 'diskUsage'.
-- Every URI parameter accepted by @POST \/{index}/_disk_usage@ is
-- exposed via 'DiskUsageOptions'; @run_expensive_tasks@ is always
-- rendered (it is required by the API).
diskUsageWith ::
  DiskUsageOptions ->
  IndexName ->
  BHRequest StatusDependant DiskUsageResponse
diskUsageWith opts indexName =
  post endpoint emptyBody
  where
    endpoint =
      [unIndexName indexName, "_disk_usage"]
        `withQueries` diskUsageOptionsParams opts

-- | 'fieldUsageStats' reports, per shard and field, how often each
-- structure (inverted index, stored fields, doc values, ...) was
-- accessed by searches. Maps to
-- @GET \/{index}/_field_usage_stats@ and returns a
-- 'FieldUsageStatsResponse'.
--
-- This is an ES-only feature (technical preview). Equivalent to
-- @'fieldUsageStatsWith' 'defaultFieldUsageStatsOptions'@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/field-usage-stats.html>.
--
-- @since 0.26.0.0
fieldUsageStats ::
  IndexName ->
  BHRequest StatusDependant FieldUsageStatsResponse
fieldUsageStats = fieldUsageStatsWith defaultFieldUsageStatsOptions

-- | 'fieldUsageStatsWith' is the fully-parameterised form of
-- 'fieldUsageStats'. Every URI parameter accepted by
-- @GET \/{index}/_field_usage_stats@ is exposed via
-- 'FieldUsageStatsOptions'.
fieldUsageStatsWith ::
  FieldUsageStatsOptions ->
  IndexName ->
  BHRequest StatusDependant FieldUsageStatsResponse
fieldUsageStatsWith opts indexName =
  get endpoint
  where
    endpoint =
      [unIndexName indexName, "_field_usage_stats"]
        `withQueries` fieldUsageStatsOptionsParams opts

-- $scriptMetadataOverview
--
-- The @/_script_context@ and @/_script_language@ endpoints expose the
-- cluster's script-engine catalogue: the execution contexts available
-- to Painless \/ expression scripts and, per language, the contexts in
-- which each may run. They are cluster-level GETs that take no path
-- arguments, no query parameters and no request body. @/_script_context@
-- is shared verbatim by Elasticsearch (7.x\/8.x\/9.x) and OpenSearch
-- (1.x\/2.x\/3.x); @/_script_language@ is served by Elasticsearch
-- (7.x\/8.x\/9.x) and OpenSearch (2.x\/3.x) but returns HTTP 500 on
-- OpenSearch 1.3 (a server bug).
--
-- The endpoints are undocumented on the 7.17 docs site
-- (@painless-contexts.html@ is 404) but are present on live 7.17.25
-- clusters. See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/painless-api-reference.html>.

-- | 'getScriptContexts' lists every Painless \/ expression execution
-- context the cluster knows about, together with the methods (and
-- their parameter signatures) each context exposes. Maps to
-- @GET /_script_context@.
--
-- @since 0.26.0.0
getScriptContexts ::
  BHRequest StatusDependant ScriptContextsResponse
getScriptContexts = get ["_script_context"]

-- | 'getScriptLanguages' lists the script languages the cluster
-- supports and, per language, the execution contexts in which each may
-- run. Maps to @GET /_script_language@.
--
-- Note: on OpenSearch 1.3 this endpoint returns HTTP 500 (a server
-- bug); calls against a 1.3 cluster will fail.
--
-- @since 0.26.0.0
getScriptLanguages ::
  BHRequest StatusDependant ScriptLanguagesResponse
getScriptLanguages = get ["_script_language"]

-- | 'deleteIndex' will delete an index given a 'Server' and an 'IndexName'.
--
-- >>> _ <- runBH' $ createIndex defaultIndexSettings (IndexName "didimakeanindex")
-- >>> response <- runBH' $ deleteIndex (IndexName "didimakeanindex")
-- >>> isSuccess response
-- True
-- >>> runBH' $ indexExists (IndexName "didimakeanindex")
-- False
deleteIndex :: IndexName -> BHRequest StatusDependant Acknowledged
deleteIndex indexName =
  delete [unIndexName indexName]

-- | 'updateIndexSettings' will apply a non-empty list of setting updates to an index
--
-- >>> _ <- runBH' $ createIndex defaultIndexSettings (IndexName "unconfiguredindex")
-- >>> response <- runBH' $ updateIndexSettings (BlocksWrite False :| []) (IndexName "unconfiguredindex")
-- >>> isSuccess response
-- True
--
-- Equivalent to @'updateIndexSettingsWith' updates 'defaultUpdateIndexSettingsOptions'@.
-- Use the @With@ variant to pass @master_timeout@, @timeout@,
-- @preserve_existing@ or @flat_settings@.
updateIndexSettings ::
  NonEmpty UpdatableIndexSetting ->
  IndexName ->
  BHRequest StatusIndependant Acknowledged
updateIndexSettings updates indexName =
  updateIndexSettingsWith updates indexName defaultUpdateIndexSettingsOptions

-- | 'updateIndexSettingsWith' is the fully-parameterised form of
-- 'updateIndexSettings'. Every URI parameter accepted by
-- @PUT /<index>/_settings@ is exposed via 'UpdateIndexSettingsOptions';
-- pass 'defaultUpdateIndexSettingsOptions' to reproduce the legacy
-- behaviour (no parameters).
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-update-settings.html>)
updateIndexSettingsWith ::
  NonEmpty UpdatableIndexSetting ->
  IndexName ->
  UpdateIndexSettingsOptions ->
  BHRequest StatusIndependant Acknowledged
updateIndexSettingsWith updates indexName opts =
  put endpoint (encode body)
  where
    endpoint =
      [unIndexName indexName, "_settings"]
        `withQueries` updateIndexSettingsOptionsParams opts
    body = Object (deepMerge [u | Object u <- toJSON <$> toList updates])

-- | 'getIndexSettings' retrieves the live settings of a single index.
-- Wraps @GET /<index>/_settings@.
--
-- Equivalent to @'getIndexSettingsWith' name 'defaultGetIndexSettingsOptions'@.
-- Use the @With@ variant to pass @master_timeout@, @flat_settings@,
-- @include_defaults@ or @local@.
getIndexSettings :: IndexName -> BHRequest StatusDependant IndexSettingsSummary
getIndexSettings indexName =
  getIndexSettingsWith indexName defaultGetIndexSettingsOptions

-- | 'getIndexSettingsWith' is the fully-parameterised form of
-- 'getIndexSettings'. Every URI parameter accepted by
-- @GET /<index>/_settings@ is exposed via 'GetIndexSettingsOptions';
-- pass 'defaultGetIndexSettingsOptions' to reproduce the legacy
-- behaviour (no parameters).
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-get-settings.html>)
getIndexSettingsWith ::
  IndexName ->
  GetIndexSettingsOptions ->
  BHRequest StatusDependant IndexSettingsSummary
getIndexSettingsWith indexName opts =
  get ([unIndexName indexName, "_settings"] `withQueries` getIndexSettingsOptionsParams opts)

-- | 'getIndexStats' returns statistics for a single index — the
-- @GET \/\<index\>\/_stats@ endpoint
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-stats.html>).
-- The response covers indexing, search, get, merge, flush, refresh,
-- query_cache, fielddata, docs, store, translog, segments and completion
-- stats; only 'docs' and 'store' are typed on this first cut, the rest
-- being preserved verbatim in 'indexStatMetricsOther'.
getIndexStats :: IndexName -> BHRequest StatusDependant IndexStats
getIndexStats indexName =
  get [unIndexName indexName, "_stats"]

-- | 'getIndex' fetches the full definition of a single index — its
-- @aliases@, @mappings@ and @settings@ — in one request. Wraps the
-- @GET \/\<index\>@ endpoint
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-get-index.html>).
-- Use 'getIndexSettings' when only the settings are needed; 'getIndex'
-- is the right choice when mappings or aliases are also required.
getIndex :: IndexName -> BHRequest StatusDependant IndexInfo
getIndex indexName =
  get [unIndexName indexName]

-- | 'getIndexRecovery' returns information about ongoing and completed
-- shard recoveries for the given index (e.g. after snapshot restore,
-- replica allocation or peer recovery on node restart). Wraps the
-- @GET \/\<index\>\/_recovery@ endpoint
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-recovery.html>).
getIndexRecovery :: IndexName -> BHRequest StatusDependant IndexRecovery
getIndexRecovery indexName =
  get [unIndexName indexName, "_recovery"]

-- | 'getIndexSegments' returns low-level Lucene segment information
-- (one entry per shard copy: primary and each replica) for the given
-- index. Wraps the @GET \/\<index\>\/_segments@ endpoint
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-segments.html>,
-- also implemented by OpenSearch — see
-- <https://docs.opensearch.org/latest/api-reference/index-apis/segments/>).
getIndexSegments :: IndexName -> BHRequest StatusDependant IndexSegments
getIndexSegments indexName =
  get [unIndexName indexName, "_segments"]

-- | 'getShardStores' returns store information for every shard copy
-- (primary and each replica) of the given index — where each copy is
-- allocated and its allocation state. Wraps the
-- @GET \/\<index\>\/_shard_stores@ endpoint
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-shards-stores.html>,
-- also implemented by OpenSearch — see
-- <https://docs.opensearch.org/latest/api-reference/index-apis/shard-stores/>).
--
-- Equivalent to @'getShardStoresWith' 'defaultShardStoresOptions'@. Use
-- the @With@ variant to pass @status@ (cluster-health filter),
-- @expand_wildcards@, @ignore_unavailable@ or @allow_no_indices@.
getShardStores :: IndexName -> BHRequest StatusDependant ShardStores
getShardStores indexName =
  getShardStoresWith indexName defaultShardStoresOptions

-- | Like 'getShardStores' but additionally accepts 'ShardStoresOptions'
-- rendered as URI parameters. 'defaultShardStoresOptions' makes this
-- byte-for-byte identical to 'getShardStores'.
getShardStoresWith ::
  IndexName ->
  ShardStoresOptions ->
  BHRequest StatusDependant ShardStores
getShardStoresWith indexName opts =
  get ([unIndexName indexName, "_shard_stores"] `withQueries` shardStoresOptionsParams opts)

-- | 'resolveIndex' resolves indices, aliases and data streams matching
-- the given patterns. Wraps the @GET /_resolve/index\/{name}@ endpoint
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-resolve-index-api.html>,
-- also implemented by OpenSearch — see
-- <https://docs.opensearch.org/latest/api-reference/index-apis/resolve-index/>).
--
-- The patterns are joined with @,@ into a single path segment, matching
-- the server's comma-separated multi-target syntax. An empty list is
-- treated as the wildcard @*@ (resolve every index the caller can
-- see). Individual patterns may be literal index names, glob
-- expressions (e.g. @logs-*@), alias names, data-stream names, or
-- @\<cluster\>:\<name\>@ remote-cluster references — the server does
-- the expansion.
--
-- Equivalent to @'resolveIndexWith' 'defaultResolveIndexOptions'@. Use
-- the @With@ variant to pass @expand_wildcards@ (e.g. to surface
-- closed or hidden indices), @ignore_unavailable@ or
-- @allow_no_indices@.
--
-- This builder is 'StatusDependant' so genuine server-side failures
-- (auth, malformed pattern, 5xx, ...) surface as an 'EsError'. Note
-- that on most backends a missing concrete target does /not/ produce
-- a 404 — the server returns 200 with empty @indices@/@aliases@\/
-- @data_streams@ arrays. Use the @ignore_unavailable@ option on
-- 'ResolveIndexOptions' (documented on ES 8.x+, silently accepted by
-- OpenSearch) if you need the server to reject unavailable targets.
resolveIndex ::
  [IndexPattern] ->
  BHRequest StatusDependant ResolvedIndices
resolveIndex = resolveIndexWith defaultResolveIndexOptions

-- | Like 'resolveIndex' but additionally accepts 'ResolveIndexOptions'
-- rendered as URI parameters. Use 'defaultResolveIndexOptions' to send
-- no parameters at all, in which case the emitted request is
-- byte-for-byte identical to 'resolveIndex'.
resolveIndexWith ::
  ResolveIndexOptions ->
  [IndexPattern] ->
  BHRequest StatusDependant ResolvedIndices
resolveIndexWith opts patterns =
  get $ ["_resolve", "index", renderedPatterns] `withQueries` resolveIndexOptionsParams opts
  where
    renderedPatterns =
      if null patterns
        then "*"
        else T.intercalate "," (map unIndexPattern patterns)

-- ----------------------------------------------------------------------
-- Dangling indices (bloodhound-04f.2.16)
-- GET /_dangling, POST /_dangling/{uuid}, DELETE /_dangling/{uuid}
-- Available since Elasticsearch 7.16. OpenSearch does not implement
-- these endpoints; a request against OS surfaces as an EsError.
-- ----------------------------------------------------------------------

-- | 'listDanglingIndices' returns every dangling index currently known
-- to the cluster — indices whose shard data exists on disk but that
-- have no cluster-state metadata, e.g. after a partial master-node
-- loss. Wraps @GET /_dangling@
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/dangling-index-apis.html>).
--
-- A healthy cluster returns an empty list. /Not/ implemented by
-- OpenSearch — a request against an OS backend surfaces as an
-- 'EsError' (typically 404).
listDanglingIndices :: BHRequest StatusDependant [DanglingIndex]
listDanglingIndices =
  danglingIndicesResponseList <$> get ["_dangling"]

-- | 'importDanglingIndex' re-attaches a dangling index to the cluster
-- metadata, restoring it to normal use. Wraps
-- @POST /_dangling/{uuid}@. The server requires the
-- @accept_data_loss=true@ flag — 'defaultImportDanglingIndexOptions'
-- sets it; pass it explicitly via 'importDanglingIndexWith' if you
-- need to also set @master_timeout@ or @timeout@.
importDanglingIndex ::
  DanglingIndexUuid ->
  BHRequest StatusDependant Acknowledged
importDanglingIndex uuid =
  importDanglingIndexWith uuid defaultImportDanglingIndexOptions

-- | Fully-parameterised form of 'importDanglingIndex'. Every URI
-- parameter accepted by @POST /_dangling/{uuid}@ is exposed via
-- 'ImportDanglingIndexOptions'; pass
-- 'defaultImportDanglingIndexOptions' to reproduce the bare behaviour
-- (only the required @accept_data_loss=true@).
importDanglingIndexWith ::
  DanglingIndexUuid ->
  ImportDanglingIndexOptions ->
  BHRequest StatusDependant Acknowledged
importDanglingIndexWith uuid opts =
  post endpoint emptyBody
  where
    endpoint =
      ["_dangling", unDanglingIndexUuid uuid]
        `withQueries` importDanglingIndexOptionsParams opts

-- | 'deleteDanglingIndex' permanently deletes the on-disk data of a
-- dangling index that the cluster will /not/ be re-importing. Wraps
-- @DELETE /_dangling/{uuid}@. Use 'deleteDanglingIndexWith' to pass
-- @master_timeout@ or @timeout@.
deleteDanglingIndex ::
  DanglingIndexUuid ->
  BHRequest StatusDependant Acknowledged
deleteDanglingIndex uuid =
  deleteDanglingIndexWith uuid defaultDeleteDanglingIndexOptions

-- | Fully-parameterised form of 'deleteDanglingIndex'. Every URI
-- parameter accepted by @DELETE /_dangling/{uuid}@ is exposed via
-- 'DeleteDanglingIndexOptions'; pass
-- 'defaultDeleteDanglingIndexOptions' to send no parameters at all.
deleteDanglingIndexWith ::
  DanglingIndexUuid ->
  DeleteDanglingIndexOptions ->
  BHRequest StatusDependant Acknowledged
deleteDanglingIndexWith uuid opts =
  delete (["_dangling", unDanglingIndexUuid uuid] `withQueries` deleteDanglingIndexOptionsParams opts)

-- | 'forceMergeIndex'
--
-- The force merge API allows to force merging of one or more indices through
-- an API. The merge relates to the number of segments a Lucene index holds
-- within each shard. The force merge operation allows to reduce the number of
-- segments by merging them.
--
-- This call will block until the merge is complete. If the http connection is
-- lost, the request will continue in the background, and any new requests will
-- block until the previous force merge is complete.

-- For more information see
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-forcemerge.html#indices-forcemerge>.
-- Nothing
-- worthwhile comes back in the response body, so matching on the status
-- should suffice.
--
-- 'forceMergeIndex' with a maxNumSegments of 1 and onlyExpungeDeletes
-- to True is the main way to release disk space back to the OS being
-- held by deleted documents.
--
-- >>> let ixn = IndexName "unoptimizedindex"
-- >>> _ <- runBH' $ deleteIndex ixn >> createIndex defaultIndexSettings ixn
-- >>> response <- runBH' $ forceMergeIndex (IndexList (ixn :| [])) (defaultIndexOptimizationSettings { maxNumSegments = Just 1, onlyExpungeDeletes = True })
-- >>> isSuccess response
-- True
forceMergeIndex :: IndexSelection -> ForceMergeIndexSettings -> BHRequest StatusDependant ShardsResult
forceMergeIndex ixs ForceMergeIndexSettings {..} =
  post endpoint emptyBody
  where
    endpoint = [indexName, "_forcemerge"] `withQueries` params
    params =
      catMaybes
        [ ("max_num_segments",) . Just . showText <$> maxNumSegments,
          Just ("only_expunge_deletes", Just (boolQP onlyExpungeDeletes)),
          Just ("flush", Just (boolQP flushAfterOptimize))
        ]
    indexName = indexSelectionName ixs

deepMerge :: [Object] -> Object
deepMerge = LS.foldl' (X.unionWith merge) mempty
  where
    merge (Object a) (Object b) = Object (deepMerge [a, b])
    merge _ b = b

-- | 'doesExist' issues a HEAD request against the given endpoint and
-- returns 'True' on any 2xx response, 'False' otherwise. Used by
-- 'indexExists', 'aliasExists', 'templateExists' and the
-- version-specific @*Exists@ helpers. Note that any non-2xx status
-- (including 5xx server errors) decodes to 'False'.
doesExist :: Endpoint -> BHRequest StatusDependant Bool
doesExist =
  withBHResponse_ isSuccess . head' @StatusDependant @IgnoredBody

-- | 'indexExists' enables you to check if an index exists. Returns 'Bool'
--  in IO
--
-- >>> exists <- runBH' $ indexExists testIndex
indexExists :: IndexName -> BHRequest StatusDependant Bool
indexExists indexName =
  doesExist [unIndexName indexName]

-- | 'refreshIndex' will force a refresh on an index. You must
-- do this if you want to read what you wrote.
--
-- >>> _ <- runBH' $ createIndex defaultIndexSettings testIndex
-- >>> _ <- runBH' $ refreshIndex testIndex
--
-- Equivalent to @'refreshIndexWith' 'defaultRefreshIndexOptions'@. Use the
-- @With@ variant to pass @ignore_unavailable@, @allow_no_indices@,
-- @expand_wildcards@ or @force@.
--
-- Returns 'ShardsResult' (the @{\"_shards\": {...}}@ envelope) because
-- ES\/OS wraps the shard stats in a top-level @_shards@ key. The inner
-- 'ShardResult' is reachable via 'srShards'.
refreshIndex :: IndexName -> BHRequest StatusDependant ShardsResult
refreshIndex = refreshIndexWith defaultRefreshIndexOptions

-- | 'refreshIndexWith' is the fully-parameterised form of 'refreshIndex'.
-- Every URI parameter accepted by @POST /<index>/_refresh@ is exposed via
-- 'RefreshIndexOptions'; pass 'defaultRefreshIndexOptions' to reproduce the
-- legacy behaviour (no parameters). (@force@ has been a no-op server-side
-- since ES 2.1 but is still accepted by every backend.)
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-refresh.html>)
refreshIndexWith ::
  RefreshIndexOptions ->
  IndexName ->
  BHRequest StatusDependant ShardsResult
refreshIndexWith opts indexName =
  post endpoint emptyBody
  where
    endpoint =
      [unIndexName indexName, "_refresh"]
        `withQueries` refreshIndexOptionsParams opts

-- | Block until the index becomes available for indexing
--  documents. This is useful for integration tests in which
--  indices are rapidly created and deleted.
--
--  The previously hard-coded @wait_for_status=yellow@ and @timeout=10s@
--  parameters are now rendered through 'clusterHealthOptionsParams', but
--  the on-the-wire request and the 'StatusIndependant' / 'HealthStatus'
--  contract are preserved: a 503 returned when the index cannot reach
--  yellow within the timeout is still surfaced as a normal response
--  rather than an error.
waitForYellowIndex :: IndexName -> BHRequest StatusIndependant HealthStatus
waitForYellowIndex indexName =
  get endpoint
  where
    endpoint =
      ["_cluster", "health", unIndexName indexName]
        `withQueries` clusterHealthOptionsParams opts
    opts =
      defaultClusterHealthOptions
        { choWaitForStatus = Just ClusterHealthYellow,
          choTimeout = Just (TimeUnitSeconds, 10)
        }

-- | 'rolloverIndex' rolls an alias over to a new index when the
-- conditions on the current write index are met. The alias must point
-- at exactly one write index whose name ends in a number and increments
-- by one (e.g. @logs-000001@ -> @logs-000002@). Wraps
-- @POST /\<alias\>/_rollover@.
--
-- Pass @'Just' conditions@ to set rollover thresholds; pass 'Nothing'
-- to rollover unconditionally (the server may still refuse if the
-- alias has no write index). The full set of body fields
-- (@settings@, @mappings@, @aliases@ for the new index) and URI
-- parameters (@dry_run@, @master_timeout@, @timeout@,
-- @wait_for_active_shards@) are not modelled here.
--
-- See <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-rollover-index.html>
-- and <https://docs.opensearch.org/latest/api-reference/index-apis/rollover-index/>.
--
-- @since 0.26.0.0
rolloverIndex ::
  IndexAliasName ->
  Maybe RolloverConditions ->
  BHRequest StatusDependant RolloverResponse
rolloverIndex alias mConditions =
  post endpoint body
  where
    endpoint = [unIndexName (indexAliasName alias), "_rollover"]
    body =
      maybe
        emptyBody
        (encode . object . pure . ("conditions" .=))
        mConditions

-- $resize
--
-- Shrink, split and clone are three near-identical endpoints that all
-- take a source index, a target index name and the same body\/URI
-- parameters as 'createIndexOptions'. Each delegates to
-- 'createIndexOptionsBody' \/ 'createIndexOptionsParams' after
-- unwrapping the typed settings newtype. Endpoint reference URLs live
-- next to the types in
-- "Database.Bloodhound.Internal.Versions.Common.Types.Indices".

-- | Shrink an existing index into a new index with fewer primary shards.
-- Maps to @POST /<source>/_shrink/<target>@ and returns 'Acknowledged'
-- on success. The source index must be made read-only
-- (@index.blocks.write: true@) and every primary shard must be resident
-- on a single node before the call; the target's @number_of_shards@
-- must be a divisor of the source's. Pass @settings@ via
-- 'shrinkSettingsOptions' to override the target's shard count.
--
-- See <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-shrink-index.html>
-- and <https://docs.opensearch.org/latest/api-reference/index-apis/shrink-index/>.
--
-- @since 0.26.0.0
shrinkIndex ::
  IndexName ->
  IndexName ->
  ShrinkSettings ->
  BHRequest StatusDependant Acknowledged
shrinkIndex source target =
  resizeEndpoint source target "_shrink" . shrinkSettingsOptions

-- | Split an existing index into a new index with more primary shards.
-- Maps to @POST /<source>/_split/<target>@ and returns 'Acknowledged'
-- on success. The source index must be read-only
-- (@index.blocks.write: true@); the target's @number_of_shards@ must
-- be a multiple of the source's, and the source's
-- @index.number_of_routing_shards@ must be a multiple of the target's.
-- Pass @settings@ via 'splitSettingsOptions' to set the target shard
-- count.
--
-- See <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-split-index.html>
-- and <https://docs.opensearch.org/latest/api-reference/index-apis/split-index/>.
--
-- @since 0.26.0.0
splitIndex ::
  IndexName ->
  IndexName ->
  SplitSettings ->
  BHRequest StatusDependant Acknowledged
splitIndex source target =
  resizeEndpoint source target "_split" . splitSettingsOptions

-- | Clone an existing index into a new index with the same mappings and
-- settings. Maps to @POST /<source>/_clone/<target>@ and returns
-- 'Acknowledged' on success. The source index must be read-only
-- (@index.blocks.write: true@); the target inherits the source's
-- @number_of_shards@.
--
-- See <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-clone-index.html>
-- and <https://docs.opensearch.org/latest/api-reference/index-apis/clone-index/>.
--
-- @since 0.26.0.0
cloneIndex ::
  IndexName ->
  IndexName ->
  CloneSettings ->
  BHRequest StatusDependant Acknowledged
cloneIndex source target =
  resizeEndpoint source target "_clone" . cloneSettingsOptions

-- | Shared request builder for the shrink\/split\/clone endpoints.
-- Path: @POST /<source>/<verb>/<target>@; body and URI parameters come
-- from the wrapped 'CreateIndexOptions'.
resizeEndpoint ::
  IndexName ->
  IndexName ->
  Text ->
  CreateIndexOptions ->
  BHRequest StatusDependant Acknowledged
resizeEndpoint source target verb opts =
  post endpoint body
  where
    endpoint =
      [unIndexName source, verb, unIndexName target]
        `withQueries` createIndexOptionsParams opts
    body = fromMaybe emptyBody (createIndexOptionsBody opts)

openOrCloseIndexes :: OpenCloseIndex -> IndexName -> BHRequest StatusIndependant Acknowledged
openOrCloseIndexes oci = openOrCloseIndexesWith oci defaultOpenCloseIndexOptions

-- | Shared request builder for the open\/close endpoints, taking the full
-- 'OpenCloseIndexOptions' parameter set. Path:
-- @POST /<index>/_open@ (or @_close@); URI parameters come from
-- 'openCloseIndexOptionsParams'.
openOrCloseIndexesWith ::
  OpenCloseIndex ->
  OpenCloseIndexOptions ->
  IndexName ->
  BHRequest StatusIndependant Acknowledged
openOrCloseIndexesWith oci opts indexName =
  post endpoint emptyBody
  where
    endpoint =
      [unIndexName indexName, stringifyOCIndex]
        `withQueries` openCloseIndexOptionsParams opts
    stringifyOCIndex = case oci of
      OpenIndex -> "_open"
      CloseIndex -> "_close"

-- | 'openIndexWith' is the fully-parameterised form of 'openIndex'. Every
-- URI parameter accepted by @POST /<index>/_open@ is exposed via
-- 'OpenCloseIndexOptions'; pass 'defaultOpenCloseIndexOptions' to
-- reproduce the legacy behaviour (no parameters).
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-open-close.html>)
openIndexWith ::
  OpenCloseIndexOptions ->
  IndexName ->
  BHRequest StatusIndependant Acknowledged
openIndexWith = openOrCloseIndexesWith OpenIndex

-- | 'closeIndexWith' is the fully-parameterised form of 'closeIndex'. Every
-- URI parameter accepted by @POST /<index>/_close@ is exposed via
-- 'OpenCloseIndexOptions'; pass 'defaultOpenCloseIndexOptions' to
-- reproduce the legacy behaviour (no parameters).
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-open-close.html>)
closeIndexWith ::
  OpenCloseIndexOptions ->
  IndexName ->
  BHRequest StatusIndependant Acknowledged
closeIndexWith = openOrCloseIndexesWith CloseIndex

-- | 'openIndex' opens an index given a 'Server' and an 'IndexName'. Explained in further detail at
--  <http://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-open-close.html>
--
-- >>> response <- runBH' $ openIndex testIndex
--
-- Equivalent to @'openIndexWith' 'defaultOpenCloseIndexOptions'@. Use the
-- @With@ variant to pass @wait_for_active_shards@, @ignore_unavailable@,
-- @allow_no_indices@, @expand_wildcards@, @master_timeout@ or @timeout@.
openIndex :: IndexName -> BHRequest StatusIndependant Acknowledged
openIndex = openOrCloseIndexes OpenIndex

-- | 'closeIndex' closes an index given a 'Server' and an 'IndexName'. Explained in further detail at
--  <http://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-open-close.html>
--
-- >>> response <- runBH' $ closeIndex testIndex
--
-- Equivalent to @'closeIndexWith' 'defaultOpenCloseIndexOptions'@. Use the
-- @With@ variant to pass @wait_for_active_shards@, @ignore_unavailable@,
-- @allow_no_indices@, @expand_wildcards@, @master_timeout@ or @timeout@.
closeIndex :: IndexName -> BHRequest StatusIndependant Acknowledged
closeIndex = openOrCloseIndexes CloseIndex

-- | 'listIndices' returns a list of all index names on a given 'Server'
listIndices :: BHRequest StatusDependant [IndexName]
listIndices =
  map unListedIndexName <$> get ["_cat/indices?format=json"]

newtype ListedIndexName = ListedIndexName {unListedIndexName :: IndexName}
  deriving stock (Eq, Show)

instance FromJSON ListedIndexName where
  parseJSON =
    withObject "ListedIndexName" $ \o ->
      ListedIndexName <$> o .: "index"

-- | 'catIndices' returns a list of all index names on a given 'Server' as well as their doc counts
catIndices :: BHRequest StatusDependant [(IndexName, Int)]
catIndices =
  map unListedIndexNameWithCount <$> get ["_cat/indices?format=json"]

newtype ListedIndexNameWithCount = ListedIndexNameWithCount {unListedIndexNameWithCount :: (IndexName, Int)}
  deriving stock (Eq, Show)

instance FromJSON ListedIndexNameWithCount where
  parseJSON =
    withObject "ListedIndexNameWithCount" $ \o -> do
      xs <- (,) <$> o .: "index" <*> o .: "docs.count"
      return $ ListedIndexNameWithCount xs

-- | 'catIndicesWith' is the fully-parameterised form of 'catIndices'.
-- Every URI parameter accepted by @GET /_cat/indices@ is exposed via
-- 'CatIndicesOptions'; pass 'defaultCatIndicesOptions' to reproduce the
-- legacy behaviour (no parameters besides @format=json@). The response
-- is a structured 'CatIndicesRow' per index, with every default column
-- typed (including 'Bytes'\/'CatBytesSize' for @store.size@) and any
-- future columns preserved verbatim in 'cirOther'.
--
-- See 'catIndicesOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-indices.html>
-- for the upstream documentation.
catIndicesWith :: CatIndicesOptions -> BHRequest StatusDependant [CatIndicesRow]
catIndicesWith opts =
  get $ ["_cat", "indices"] `withQueries` catIndicesOptionsParams opts

-- | 'catAliases' lists every index alias on the cluster via
-- @GET /_cat\/aliases?format=json@. Equivalent to
-- @'catAliasesWith' 'Nothing' 'defaultCatAliasesOptions'@.
--
-- See 'catAliasesWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-aliases.html>
-- for the upstream documentation.
catAliases :: BHRequest StatusDependant [CatAliasesRow]
catAliases = catAliasesWith Nothing defaultCatAliasesOptions

-- | 'catAliasesWith' is the fully-parameterised form of 'catAliases'.
-- The optional 'AliasName' narrows the result to a single alias name;
-- it is rendered as the @/<alias>@ path segment. (The ES spec also
-- permits comma-separated lists and wildcards in that segment; pass
-- them through by constructing an 'AliasName' wrapping an 'IndexName'
-- that already contains those characters.) Every URI parameter
-- accepted by @GET /_cat\/aliases@ is exposed via 'CatAliasesOptions';
-- pass 'defaultCatAliasesOptions' to reproduce the legacy behaviour
-- (no parameters besides @format=json@). The response is a structured
-- 'CatAliasesRow' per alias, with every default column typed (including
-- the @"-"@ sentinel for @is_write_index@) and any future columns
-- preserved verbatim in 'carOther'.
--
-- See 'catAliasesOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-aliases.html>
-- for the upstream documentation.
catAliasesWith ::
  Maybe AliasName ->
  CatAliasesOptions ->
  BHRequest StatusDependant [CatAliasesRow]
catAliasesWith mAlias opts =
  get $ path `withQueries` catAliasesOptionsParams opts
  where
    path = case mAlias of
      Nothing -> ["_cat", "aliases"]
      Just (AliasName name) -> ["_cat", "aliases", unIndexName name]

-- | 'catAllocation' lists the allocation of disk space for indexes and
-- the number of shards on each data node via
-- @GET /_cat\/allocation?format=json@. Equivalent to
-- @'catAllocationWith' 'Nothing' 'defaultCatAllocationOptions'@.
--
-- See 'catAllocationWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-allocation.html>
-- for the upstream documentation.
catAllocation :: BHRequest StatusDependant [CatAllocationRow]
catAllocation = catAllocationWith Nothing defaultCatAllocationOptions

-- | 'catAllocationWith' is the fully-parameterised form of
-- 'catAllocation'. The optional 'NodeName' narrows the result to a
-- specific node; it is rendered as the @/<node_id>@ path segment. (The
-- ES\/OS spec also permits comma-separated lists and wildcards in that
-- segment; pass them through by constructing a 'NodeName' that already
-- contains those characters.) Every URI parameter accepted by
-- @GET /_cat\/allocation@ is exposed via 'CatAllocationOptions'; pass
-- 'defaultCatAllocationOptions' to reproduce the legacy behaviour (no
-- parameters besides @format=json@). The response is a structured
-- 'CatAllocationRow' per node, with every default column typed
-- (including 'Bytes'\/'CatBytesSize' for the @disk.*@ columns, and the
-- dedicated 'CatShardsUndesired' sum for the @shards.undesired@ @-1@
-- sentinel) and any future columns preserved verbatim in 'calrOther'.
--
-- See 'catAllocationOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-allocation.html>
-- for the upstream documentation.
catAllocationWith ::
  Maybe NodeName ->
  CatAllocationOptions ->
  BHRequest StatusDependant [CatAllocationRow]
catAllocationWith mNode opts =
  get $ path `withQueries` catAllocationOptionsParams opts
  where
    path = case mNode of
      Nothing -> ["_cat", "allocation"]
      Just name -> ["_cat", "allocation", nodeName name]

-- | 'catCount' reports the document count for the whole cluster via
-- @GET /_cat\/count?format=json@. Equivalent to
-- @'catCountWith' 'Nothing' 'defaultCatCountOptions'@.
--
-- See 'catCountWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-count.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catCount :: BHRequest StatusDependant [CatCountRow]
catCount = catCountWith Nothing defaultCatCountOptions

-- | 'catCountWith' is the fully-parameterised form of 'catCount'. The
-- optional 'IndexName' narrows the count to a single index; it is
-- rendered as the @/<index>@ path segment. (The ES\/OS spec also
-- permits comma-separated lists and wildcards in that segment; pass
-- them through by constructing an 'IndexName' that already contains
-- those characters.) Every URI parameter accepted by
-- @GET /_cat\/count@ is exposed via 'CatCountOptions'; pass
-- 'defaultCatCountOptions' to reproduce the legacy behaviour (no
-- parameters besides @format=json@). The response is a structured
-- 'CatCountRow', with every default column typed and any future columns
-- preserved verbatim in 'ccrOther'.
--
-- See 'catCountOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-count.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catCountWith ::
  Maybe IndexName ->
  CatCountOptions ->
  BHRequest StatusDependant [CatCountRow]
catCountWith mIndex opts =
  get $ path `withQueries` catCountOptionsParams opts
  where
    path = case mIndex of
      Nothing -> ["_cat", "count"]
      Just name -> ["_cat", "count", unIndexName name]

-- | 'catMaster' reports the identity of the elected master node via
-- @GET /_cat\/master?format=json@. Equivalent to
-- @'catMasterWith' 'defaultCatMasterOptions'@.
--
-- See 'catMasterWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-master.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catMaster :: BHRequest StatusDependant [CatMasterRow]
catMaster = catMasterWith defaultCatMasterOptions

-- | 'catMasterWith' is the fully-parameterised form of 'catMaster'.
-- Every URI parameter accepted by @GET /_cat\/master@ is exposed via
-- 'CatMasterOptions'; pass 'defaultCatMasterOptions' to reproduce the
-- legacy behaviour (no parameters besides @format=json@). The response
-- is a structured 'CatMasterRow', with every default column typed and
-- any future columns preserved verbatim in 'cmrOther'.
--
-- See 'catMasterOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-master.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catMasterWith ::
  CatMasterOptions ->
  BHRequest StatusDependant [CatMasterRow]
catMasterWith opts =
  get $ ["_cat", "master"] `withQueries` catMasterOptionsParams opts

-- | 'catHealth' reports a one-row-per-snapshot summary of cluster
-- health via @GET /_cat\/health?format=json@. Equivalent to
-- @'catHealthWith' 'defaultCatHealthOptions'@.
--
-- See 'catHealthWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-health.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catHealth :: BHRequest StatusDependant [CatHealthRow]
catHealth = catHealthWith defaultCatHealthOptions

-- | 'catHealthWith' is the fully-parameterised form of 'catHealth'.
-- Every URI parameter accepted by @GET /_cat\/health@ is exposed via
-- 'CatHealthOptions'; pass 'defaultCatHealthOptions' to reproduce the
-- legacy behaviour (no parameters besides @format=json@). The response
-- is a structured 'CatHealthRow', with every default column typed and
-- any future columns preserved verbatim in 'chrOther'.
--
-- See 'catHealthOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-health.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catHealthWith ::
  CatHealthOptions ->
  BHRequest StatusDependant [CatHealthRow]
catHealthWith opts =
  get $ ["_cat", "health"] `withQueries` catHealthOptionsParams opts

-- | 'catPendingTasks' lists the cluster-level tasks waiting to be
-- executed via @GET /_cat\/pending_tasks?format=json@. Equivalent to
-- @'catPendingTasksWith' 'defaultCatPendingTasksOptions'@. The result
-- list is empty when the cluster is idle.
--
-- See 'catPendingTasksWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-pending-tasks.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catPendingTasks :: BHRequest StatusDependant [CatPendingTasksRow]
catPendingTasks = catPendingTasksWith defaultCatPendingTasksOptions

-- | 'catPendingTasksWith' is the fully-parameterised form of
-- 'catPendingTasks'. Every URI parameter accepted by
-- @GET /_cat\/pending_tasks@ is exposed via 'CatPendingTasksOptions';
-- pass 'defaultCatPendingTasksOptions' to reproduce the legacy
-- behaviour (no parameters besides @format=json@). The response is a
-- structured 'CatPendingTasksRow' per pending task, with every default
-- column typed and any future columns preserved verbatim in
-- 'cptrOther'.
--
-- See 'catPendingTasksOptionsParams' for the rendering of each field,
-- and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-pending-tasks.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catPendingTasksWith ::
  CatPendingTasksOptions ->
  BHRequest StatusDependant [CatPendingTasksRow]
catPendingTasksWith opts =
  get $ ["_cat", "pending_tasks"] `withQueries` catPendingTasksOptionsParams opts

-- | 'catPlugins' lists the plugins installed on each node via
-- @GET /_cat\/plugins?format=json@. Equivalent to
-- @'catPluginsWith' 'defaultCatPluginsOptions'@. The result list is
-- empty on a cluster with no plugins installed.
--
-- See 'catPluginsWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-plugins.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catPlugins :: BHRequest StatusDependant [CatPluginsRow]
catPlugins = catPluginsWith defaultCatPluginsOptions

-- | 'catPluginsWith' is the fully-parameterised form of 'catPlugins'.
-- Every URI parameter accepted by @GET /_cat\/plugins@ is exposed via
-- 'CatPluginsOptions'; pass 'defaultCatPluginsOptions' to reproduce the
-- legacy behaviour (no parameters besides @format=json@). The response
-- is a structured 'CatPluginsRow' per plugin, with every default column
-- typed and any future columns preserved verbatim in 'cprOther'.
--
-- See 'catPluginsOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-plugins.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catPluginsWith ::
  CatPluginsOptions ->
  BHRequest StatusDependant [CatPluginsRow]
catPluginsWith opts =
  get $ ["_cat", "plugins"] `withQueries` catPluginsOptionsParams opts

-- | 'catTemplates' lists the cluster's index templates via
-- @GET /_cat\/templates?format=json@. Equivalent to
-- @'catTemplatesWith' 'Nothing' 'defaultCatTemplatesOptions'@.
--
-- See 'catTemplatesWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-templates.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catTemplates :: BHRequest StatusDependant [CatTemplatesRow]
catTemplates = catTemplatesWith Nothing defaultCatTemplatesOptions

-- | 'catTemplatesWith' is the fully-parameterised form of 'catTemplates'.
-- The optional 'TemplateName' narrows the result to templates matching
-- a single name; it is rendered as the @/<name>@ path segment. (The
-- ES\/OS spec also permits comma-separated lists and wildcards in that
-- segment; pass them through by constructing a 'TemplateName' that
-- already contains those characters.) Every URI parameter accepted by
-- @GET /_cat\/templates@ is exposed via 'CatTemplatesOptions'; pass
-- 'defaultCatTemplatesOptions' to reproduce the legacy behaviour (no
-- parameters besides @format=json@). The response is a structured
-- 'CatTemplatesRow' per template, with every default column typed
-- (including the stringified @index_patterns@\/@composed_of@ columns,
-- kept verbatim) and any future columns preserved verbatim in
-- 'ctprOther'.
--
-- See 'catTemplatesOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-templates.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catTemplatesWith ::
  Maybe TemplateName ->
  CatTemplatesOptions ->
  BHRequest StatusDependant [CatTemplatesRow]
catTemplatesWith mName opts =
  get $ path `withQueries` catTemplatesOptionsParams opts
  where
    path = case mName of
      Nothing -> ["_cat", "templates"]
      Just (TemplateName name) -> ["_cat", "templates", name]

-- | 'catThreadPool' lists the thread pools of each node via
-- @GET /_cat\/thread_pool?format=json@. Equivalent to
-- @'catThreadPoolWith' 'Nothing' 'defaultCatThreadPoolOptions'@.
--
-- See 'catThreadPoolWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-thread-pool.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catThreadPool :: BHRequest StatusDependant [CatThreadPoolRow]
catThreadPool = catThreadPoolWith Nothing defaultCatThreadPoolOptions

-- | 'catThreadPoolWith' is the fully-parameterised form of
-- 'catThreadPool'. The optional 'IndexName' narrows the result to a
-- specific thread pool; it is rendered as the @/<thread_pool>@ path
-- segment. (The thread-pool name is a plain string with no dedicated
-- newtype; following the catAliases\/catCount convention it is carried
-- by 'IndexName' and rendered via 'unIndexName'. The ES\/OS spec also
-- permits comma-separated lists and wildcards in that segment; pass
-- them through by constructing an 'IndexName' that already contains
-- those characters.) Every URI parameter accepted by
-- @GET /_cat\/thread_pool@ is exposed via 'CatThreadPoolOptions'; pass
-- 'defaultCatThreadPoolOptions' to reproduce the legacy behaviour (no
-- parameters besides @format=json@). The response is a structured
-- 'CatThreadPoolRow' per pool, with every default column typed
-- (including @null@-tolerance for the @core@, @max@ and @keep_alive@
-- columns of non-@fixed@ pool types) and any future columns preserved
-- verbatim in 'ctprlOther'.
--
-- See 'catThreadPoolOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-thread-pool.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catThreadPoolWith ::
  Maybe IndexName ->
  CatThreadPoolOptions ->
  BHRequest StatusDependant [CatThreadPoolRow]
catThreadPoolWith mPool opts =
  get $ path `withQueries` catThreadPoolOptionsParams opts
  where
    path = case mPool of
      Nothing -> ["_cat", "thread_pool"]
      Just name -> ["_cat", "thread_pool", unIndexName name]

-- | 'catFielddata' lists the amount of heap memory currently used by
-- the field data cache, per field per node, via
-- @GET /_cat\/fielddata?format=json@. Equivalent to
-- @'catFielddataWith' 'Nothing' 'defaultCatFielddataOptions'@.
--
-- See 'catFielddataWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-fielddata.html>
-- for the upstream documentation.
catFielddata :: BHRequest StatusDependant [CatFielddataRow]
catFielddata = catFielddataWith Nothing defaultCatFielddataOptions

-- | 'catFielddataWith' is the fully-parameterised form of
-- 'catFielddata'. The optional 'FieldName' narrows the result to a
-- specific field; it is rendered as the @/<fields>@ path segment. (The
-- ES\/OS spec also permits comma-separated lists and wildcards in that
-- segment; pass them through by constructing a 'FieldName' that already
-- contains those characters.) Every URI parameter accepted by
-- @GET /_cat\/fielddata@ is exposed via 'CatFielddataOptions'; pass
-- 'defaultCatFielddataOptions' to reproduce the legacy behaviour (no
-- parameters besides @format=json@). The response is a structured
-- 'CatFielddataRow' per (node, field), with every default column typed
-- (including 'Bytes'\/'CatBytesSize' for @size@) and any future columns
-- preserved verbatim in 'cfdrOther'.
--
-- See 'catFielddataOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-fielddata.html>
-- for the upstream documentation.
catFielddataWith ::
  Maybe FieldName ->
  CatFielddataOptions ->
  BHRequest StatusDependant [CatFielddataRow]
catFielddataWith mField opts =
  get $ path `withQueries` catFielddataOptionsParams opts
  where
    path = case mField of
      Nothing -> ["_cat", "fielddata"]
      Just fname -> ["_cat", "fielddata", unFieldName fname]

-- | 'catNodeattrs' lists custom node attributes via
-- @GET /_cat\/nodeattrs?format=json@. Equivalent to
-- @'catNodeattrsWith' 'defaultCatNodeattrsOptions'@. The result list is
-- empty when no custom node attributes are configured.
--
-- See 'catNodeattrsWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-nodeattrs.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catNodeattrs :: BHRequest StatusDependant [CatNodeattrsRow]
catNodeattrs = catNodeattrsWith defaultCatNodeattrsOptions

-- | 'catNodeattrsWith' is the fully-parameterised form of
-- 'catNodeattrs'. Every URI parameter accepted by
-- @GET /_cat\/nodeattrs@ is exposed via 'CatNodeattrsOptions'; pass
-- 'defaultCatNodeattrsOptions' to reproduce the legacy behaviour (no
-- parameters besides @format=json@). The response is a structured
-- 'CatNodeattrsRow' per (node, attribute), with every default column
-- typed and any future columns preserved verbatim in 'cnarOther'.
--
-- See 'catNodeattrsOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-nodeattrs.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catNodeattrsWith ::
  CatNodeattrsOptions ->
  BHRequest StatusDependant [CatNodeattrsRow]
catNodeattrsWith opts =
  get $ ["_cat", "nodeattrs"] `withQueries` catNodeattrsOptionsParams opts

-- | 'catRepositories' lists snapshot repositories registered on the
-- cluster via @GET /_cat\/repositories?format=json@. Equivalent to
-- @'catRepositoriesWith' 'defaultCatRepositoriesOptions'@. The result
-- list is empty when no repositories are registered.
--
-- See 'catRepositoriesWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-repositories.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catRepositories :: BHRequest StatusDependant [CatRepositoriesRow]
catRepositories = catRepositoriesWith defaultCatRepositoriesOptions

-- | 'catRepositoriesWith' is the fully-parameterised form of
-- 'catRepositories'. Every URI parameter accepted by
-- @GET /_cat\/repositories@ is exposed via 'CatRepositoriesOptions';
-- pass 'defaultCatRepositoriesOptions' to reproduce the legacy
-- behaviour (no parameters besides @format=json@). The response is a
-- structured 'CatRepositoriesRow' per repository, with every default
-- column typed and any future columns preserved verbatim in
-- 'crrOther'.
--
-- See 'catRepositoriesOptionsParams' for the rendering of each field,
-- and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-repositories.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catRepositoriesWith ::
  CatRepositoriesOptions ->
  BHRequest StatusDependant [CatRepositoriesRow]
catRepositoriesWith opts =
  get $ ["_cat", "repositories"] `withQueries` catRepositoriesOptionsParams opts

-- | 'catShards' lists the state of every primary and replica shard in
-- the cluster via @GET /_cat\/shards?format=json@. Equivalent to
-- @'catShardsWith' 'Nothing' 'defaultCatShardsOptions'@.
--
-- See 'catShardsWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-shards.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catShards :: BHRequest StatusDependant [CatShardsRow]
catShards = catShardsWith Nothing defaultCatShardsOptions

-- | 'catShardsWith' is the fully-parameterised form of 'catShards'.
-- The optional 'IndexName' narrows the result to a specific index (or
-- index pattern); it is rendered as the @/<index>@ path segment. (The
-- ES\/OS spec also permits comma-separated lists and wildcards in
-- that segment; pass them through by constructing an 'IndexName' that
-- already contains those characters.) Every URI parameter accepted by
-- @GET /_cat\/shards@ is exposed via 'CatShardsOptions'; pass
-- 'defaultCatShardsOptions' to reproduce the legacy behaviour (no
-- parameters besides @format=json@). The response is a structured
-- 'CatShardsRow' per shard, with every default column typed (including
-- 'Bytes'\/'CatBytesSize' for @store@) and any future columns
-- preserved verbatim in 'csrOther'.
--
-- See 'catShardsOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-shards.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catShardsWith ::
  Maybe IndexName ->
  CatShardsOptions ->
  BHRequest StatusDependant [CatShardsRow]
catShardsWith mIndex opts =
  get $ path `withQueries` catShardsOptionsParams opts
  where
    path = case mIndex of
      Nothing -> ["_cat", "shards"]
      Just idx -> ["_cat", "shards", unIndexName idx]

-- | 'catTasks' lists the tasks currently executing on the cluster via
-- @GET /_cat\/tasks?format=json@. Equivalent to
-- @'catTasksWith' 'defaultCatTasksOptions'@. The result list is empty
-- when the cluster is idle.
--
-- See 'catTasksWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-tasks.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catTasks :: BHRequest StatusDependant [CatTasksRow]
catTasks = catTasksWith defaultCatTasksOptions

-- | 'catTasksWith' is the fully-parameterised form of 'catTasks'.
-- Every URI parameter accepted by @GET /_cat\/tasks@ is exposed via
-- 'CatTasksOptions'; pass 'defaultCatTasksOptions' to reproduce the
-- legacy behaviour (no parameters besides @format=json@). The response
-- is a structured 'CatTasksRow' per task, with every default column
-- typed and any future columns preserved verbatim in 'ctrrOther'.
--
-- See 'catTasksOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-tasks.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catTasksWith ::
  CatTasksOptions ->
  BHRequest StatusDependant [CatTasksRow]
catTasksWith opts =
  get $ ["_cat", "tasks"] `withQueries` catTasksOptionsParams opts

-- | 'catSnapshots' lists the snapshots stored in every snapshot
-- repository registered on the cluster via
-- @GET /_cat\/snapshots?format=json@. Equivalent to
-- @'catSnapshotsWith' 'Nothing' 'defaultCatSnapshotsOptions'@.
--
-- See 'catSnapshotsWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-snapshots.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catSnapshots :: BHRequest StatusDependant [CatSnapshotsRow]
catSnapshots = catSnapshotsWith Nothing defaultCatSnapshotsOptions

-- | 'catSnapshotsWith' is the fully-parameterised form of
-- 'catSnapshots'. The optional 'SnapshotRepoName' narrows the result
-- to one or more repositories; it is rendered as the @/<repository>@
-- path segment. (The ES\/OS spec also permits comma-separated lists
-- and wildcards in that segment, plus the @_all@ sentinel; pass them
-- through by constructing a 'SnapshotRepoName' that already contains
-- those characters.) Every URI parameter accepted by
-- @GET /_cat\/snapshots@ is exposed via 'CatSnapshotsOptions'; pass
-- 'defaultCatSnapshotsOptions' to reproduce the legacy behaviour (no
-- parameters besides @format=json@). The response is a structured
-- 'CatSnapshotsRow' per snapshot, with every default column typed and
-- any future columns preserved verbatim in 'csnrOther'.
--
-- See 'catSnapshotsOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-snapshots.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catSnapshotsWith ::
  Maybe SnapshotRepoName ->
  CatSnapshotsOptions ->
  BHRequest StatusDependant [CatSnapshotsRow]
catSnapshotsWith mRepo opts =
  get $ path `withQueries` catSnapshotsOptionsParams opts
  where
    path = case mRepo of
      Nothing -> ["_cat", "snapshots"]
      Just repo -> ["_cat", "snapshots", snapshotRepoName repo]

-- | 'catNodes' lists the cluster topology — one row per node with its
-- identity, resource usage, and elected-master flag — via
-- @GET /_cat\/nodes?format=json@. Equivalent to
-- @'catNodesWith' 'defaultCatNodesOptions'@.
--
-- See 'catNodesWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-nodes.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catNodes :: BHRequest StatusDependant [CatNodesRow]
catNodes = catNodesWith defaultCatNodesOptions

-- | 'catNodesWith' is the fully-parameterised form of 'catNodes'. Every
-- URI parameter accepted by @GET /_cat\/nodes@ is exposed via
-- 'CatNodesOptions'; pass 'defaultCatNodesOptions' to reproduce the
-- legacy behaviour (no parameters besides @format=json@). The response
-- is a structured 'CatNodesRow' per node, with every default column
-- typed (including 'Bytes'\/'CatBytesSize' for the @heap.*@, @ram.*@,
-- and @disk.*@ columns, and the dedicated 'CatNodesMaster' sum for the
-- @master@ @\"*\"@\/@\"-\"@ sentinel) and any future columns preserved
-- verbatim in 'cnrOther'.
--
-- Unlike @_cat\/allocation@ and @_cat\/shards@, the @_cat\/nodes@
-- endpoint does not accept a path-segment filter (no
-- @GET /_cat\/nodes/\<node_id>@ form exists on either ES or OS). To
-- narrow the result, use the @h@ parameter to select columns and filter
-- the returned list client-side.
--
-- See 'catNodesOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-nodes.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catNodesWith ::
  CatNodesOptions ->
  BHRequest StatusDependant [CatNodesRow]
catNodesWith opts =
  get $ ["_cat", "nodes"] `withQueries` catNodesOptionsParams opts

-- | 'catSegments' lists the low-level Lucene segments of each shard of
-- each index via @GET /_cat\/segments?format=json@. Equivalent to
-- @'catSegmentsWith' 'Nothing' 'defaultCatSegmentsOptions'@.
--
-- See 'catSegmentsWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-segments.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catSegments :: BHRequest StatusDependant [CatSegmentsRow]
catSegments = catSegmentsWith Nothing defaultCatSegmentsOptions

-- | 'catSegmentsWith' is the fully-parameterised form of 'catSegments'.
-- The optional 'IndexName' scopes the result to a single index via
-- @GET /\<index\>/_cat\/segments?format=json@. Every URI parameter
-- accepted by the endpoint is exposed via 'CatSegmentsOptions'; pass
-- 'defaultCatSegmentsOptions' to reproduce the legacy behaviour (no
-- parameters besides @format=json@). The response is a structured
-- 'CatSegmentsRow' per segment, with every default column typed and any
-- future columns preserved verbatim in 'cserOther'.
--
-- See 'catSegmentsOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-segments.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catSegmentsWith ::
  Maybe IndexName ->
  CatSegmentsOptions ->
  BHRequest StatusDependant [CatSegmentsRow]
catSegmentsWith mIndex opts =
  get $ path `withQueries` catSegmentsOptionsParams opts
  where
    path = case mIndex of
      Nothing -> ["_cat", "segments"]
      Just idx -> [unIndexName idx, "_cat", "segments"]

-- | 'catRecovery' lists shard recovery progress for each shard of each
-- index via @GET /_cat\/recovery?format=json@. Equivalent to
-- @'catRecoveryWith' 'Nothing' 'defaultCatRecoveryOptions'@.
--
-- See 'catRecoveryWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-recovery.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catRecovery :: BHRequest StatusDependant [CatRecoveryRow]
catRecovery = catRecoveryWith Nothing defaultCatRecoveryOptions

-- | 'catRecoveryWith' is the fully-parameterised form of 'catRecovery'.
-- The optional 'IndexName' scopes the result to a single index via
-- @GET /\<index\>/_cat\/recovery?format=json@. Every URI parameter
-- accepted by the endpoint is exposed via 'CatRecoveryOptions'; pass
-- 'defaultCatRecoveryOptions' to reproduce the legacy behaviour (no
-- parameters besides @format=json@). The response is a structured
-- 'CatRecoveryRow' per recovery, with every default column typed and
-- any future columns preserved verbatim in 'cryrOther'.
--
-- See 'catRecoveryOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat-recovery.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catRecoveryWith ::
  Maybe IndexName ->
  CatRecoveryOptions ->
  BHRequest StatusDependant [CatRecoveryRow]
catRecoveryWith mIndex opts =
  get $ path `withQueries` catRecoveryOptionsParams opts
  where
    path = case mIndex of
      Nothing -> ["_cat", "recovery"]
      Just idx -> [unIndexName idx, "_cat", "recovery"]

-- | 'catComponentTemplates' lists the component templates via
-- @GET /_cat\/component_templates?format=json@. Equivalent to
-- @'catComponentTemplatesWith' 'Nothing' 'defaultCatComponentTemplatesOptions'@.
-- The optional name narrows the result (and accepts wildcards).
--
-- See 'catComponentTemplatesWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-component-templates.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catComponentTemplates :: BHRequest StatusDependant [CatComponentTemplatesRow]
catComponentTemplates =
  catComponentTemplatesWith Nothing defaultCatComponentTemplatesOptions

-- | 'catComponentTemplatesWith' is the fully-parameterised form of
-- 'catComponentTemplates'. The optional 'Text' narrows the result to a
-- component template name (comma-separated lists and wildcards are
-- accepted by the server; pass them through verbatim). Every URI
-- parameter accepted by @GET /_cat\/component_templates@ is exposed via
-- 'CatComponentTemplatesOptions'. The response is a structured
-- 'CatComponentTemplatesRow' per template; the @version@ column is
-- emitted either as the literal string @"null"@ or as a native JSON
-- @null@ (both decode safely), and any future columns are preserved
-- verbatim in 'cctrOther'.
--
-- See 'catComponentTemplatesOptionsParams' for the rendering of each
-- field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-component-templates.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catComponentTemplatesWith ::
  Maybe Text ->
  CatComponentTemplatesOptions ->
  BHRequest StatusDependant [CatComponentTemplatesRow]
catComponentTemplatesWith mName opts =
  get $ path `withQueries` catComponentTemplatesOptionsParams opts
  where
    path = case mName of
      Nothing -> ["_cat", "component_templates"]
      Just name -> ["_cat", "component_templates", name]

-- | 'catCircuitBreakers' lists the JVM circuit breakers via
-- @GET /_cat\/circuit_breaker?format=json@. Equivalent to
-- @'catCircuitBreakersWith' 'Nothing' 'defaultCatCircuitBreakersOptions'@.
--
-- See 'catCircuitBreakersWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-circuit-breaker.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catCircuitBreakers :: BHRequest StatusDependant [CatCircuitBreakersRow]
catCircuitBreakers =
  catCircuitBreakersWith Nothing defaultCatCircuitBreakersOptions

-- | 'catCircuitBreakersWith' is the fully-parameterised form of
-- 'catCircuitBreakers'. The optional 'Text' is a comma-separated list of
-- regular expressions filtering the breaker names. Every URI parameter
-- accepted by @GET /_cat\/circuit_breaker@ is exposed via
-- 'CatCircuitBreakersOptions'. The response is a structured
-- 'CatCircuitBreakersRow' per breaker, with humanised @limit@\/@estimated
-- sizes, raw-byte @_bytes@ counts, and any future columns preserved
-- verbatim in 'ccbrOther'.
--
-- See 'catCircuitBreakersOptionsParams' for the rendering of each field,
-- and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-circuit-breaker.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catCircuitBreakersWith ::
  Maybe Text ->
  CatCircuitBreakersOptions ->
  BHRequest StatusDependant [CatCircuitBreakersRow]
catCircuitBreakersWith mPatterns opts =
  get $ path `withQueries` catCircuitBreakersOptionsParams opts
  where
    path = case mPatterns of
      Nothing -> ["_cat", "circuit_breaker"]
      Just patterns -> ["_cat", "circuit_breaker", patterns]

-- | 'catMlJobs' lists the ML anomaly-detection jobs via
-- @GET /_cat\/ml\/anomaly_detectors?format=json@. Equivalent to
-- @'catMlJobsWith' 'Nothing' 'defaultCatMlJobsOptions'@.
--
-- See 'catMlJobsWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-anomaly-detectors.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catMlJobs :: BHRequest StatusDependant [CatMlJobsRow]
catMlJobs = catMlJobsWith Nothing defaultCatMlJobsOptions

-- | 'catMlJobsWith' is the fully-parameterised form of 'catMlJobs'. The
-- optional 'Text' narrows the result to a job id (wildcards accepted).
-- Every URI parameter accepted by
-- @GET /_cat\/ml\/anomaly_detectors@ is exposed via 'CatMlJobsOptions'.
-- The response is a structured 'CatMlJobsRow' per job; the commonly-used
-- columns are typed and anything else is preserved verbatim in
-- 'cmjrOther'.
--
-- See 'catMlJobsOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-anomaly-detectors.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catMlJobsWith ::
  Maybe Text ->
  CatMlJobsOptions ->
  BHRequest StatusDependant [CatMlJobsRow]
catMlJobsWith mJobId opts =
  get $ path `withQueries` catMlJobsOptionsParams opts
  where
    path = case mJobId of
      Nothing -> ["_cat", "ml", "anomaly_detectors"]
      Just jobId -> ["_cat", "ml", "anomaly_detectors", jobId]

-- | 'catMlDataFrameAnalytics' lists the ML data frame analytics jobs via
-- @GET /_cat\/ml\/data_frame\/analytics?format=json@. Equivalent to
-- @'catMlDataFrameAnalyticsWith' 'Nothing' 'defaultCatMlDataFrameAnalyticsOptions'@.
--
-- See 'catMlDataFrameAnalyticsWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-data-frame-analytics.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catMlDataFrameAnalytics ::
  BHRequest StatusDependant [CatMlDataFrameAnalyticsRow]
catMlDataFrameAnalytics =
  catMlDataFrameAnalyticsWith Nothing defaultCatMlDataFrameAnalyticsOptions

-- | 'catMlDataFrameAnalyticsWith' is the fully-parameterised form of
-- 'catMlDataFrameAnalytics'. The optional 'Text' narrows the result to an
-- analytics job id (wildcards accepted). Every URI parameter accepted by
-- @GET /_cat\/ml\/data_frame\/analytics@ is exposed via
-- 'CatMlDataFrameAnalyticsOptions'. The response is a structured
-- 'CatMlDataFrameAnalyticsRow' per job, with every default column typed
-- and any future columns preserved verbatim in 'cmfaOther'.
--
-- See 'catMlDataFrameAnalyticsOptionsParams' for the rendering of each
-- field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-data-frame-analytics.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catMlDataFrameAnalyticsWith ::
  Maybe Text ->
  CatMlDataFrameAnalyticsOptions ->
  BHRequest StatusDependant [CatMlDataFrameAnalyticsRow]
catMlDataFrameAnalyticsWith mId opts =
  get $ path `withQueries` catMlDataFrameAnalyticsOptionsParams opts
  where
    path = case mId of
      Nothing -> ["_cat", "ml", "data_frame", "analytics"]
      Just i -> ["_cat", "ml", "data_frame", "analytics", i]

-- | 'catMlDatafeeds' lists the ML datafeeds via
-- @GET /_cat\/ml\/datafeeds?format=json@. Equivalent to
-- @'catMlDatafeedsWith' 'Nothing' 'defaultCatMlDatafeedsOptions'@.
--
-- See 'catMlDatafeedsWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-datafeeds.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catMlDatafeeds :: BHRequest StatusDependant [CatMlDatafeedsRow]
catMlDatafeeds = catMlDatafeedsWith Nothing defaultCatMlDatafeedsOptions

-- | 'catMlDatafeedsWith' is the fully-parameterised form of
-- 'catMlDatafeeds'. The optional 'Text' narrows the result to a datafeed
-- id (wildcards accepted). Every URI parameter accepted by
-- @GET /_cat\/ml\/datafeeds@ is exposed via 'CatMlDatafeedsOptions'. The
-- response is a structured 'CatMlDatafeedsRow' per datafeed, with every
-- default column typed and any future columns preserved verbatim in
-- 'cmdfOther'.
--
-- See 'catMlDatafeedsOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-datafeeds.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catMlDatafeedsWith ::
  Maybe Text ->
  CatMlDatafeedsOptions ->
  BHRequest StatusDependant [CatMlDatafeedsRow]
catMlDatafeedsWith mDatafeedId opts =
  get $ path `withQueries` catMlDatafeedsOptionsParams opts
  where
    path = case mDatafeedId of
      Nothing -> ["_cat", "ml", "datafeeds"]
      Just datafeedId -> ["_cat", "ml", "datafeeds", datafeedId]

-- | 'catMlTrainedModels' lists the ML trained models via
-- @GET /_cat\/ml\/trained_models?format=json@. Equivalent to
-- @'catMlTrainedModelsWith' 'Nothing' 'defaultCatMlTrainedModelsOptions'@.
--
-- See 'catMlTrainedModelsWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-trained-models.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catMlTrainedModels :: BHRequest StatusDependant [CatMlTrainedModelsRow]
catMlTrainedModels =
  catMlTrainedModelsWith Nothing defaultCatMlTrainedModelsOptions

-- | 'catMlTrainedModelsWith' is the fully-parameterised form of
-- 'catMlTrainedModels'. The optional 'Text' narrows the result to a model
-- id (wildcards accepted). Every URI parameter accepted by
-- @GET /_cat\/ml\/trained_models@ — including the @from@\/@size@
-- pagination — is exposed via 'CatMlTrainedModelsOptions'. The response
-- is a structured 'CatMlTrainedModelsRow' per model, with every default
-- column typed and any future columns preserved verbatim in 'ctmrOther'.
--
-- See 'catMlTrainedModelsOptionsParams' for the rendering of each field,
-- and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-trained-models.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catMlTrainedModelsWith ::
  Maybe Text ->
  CatMlTrainedModelsOptions ->
  BHRequest StatusDependant [CatMlTrainedModelsRow]
catMlTrainedModelsWith mModelId opts =
  get $ path `withQueries` catMlTrainedModelsOptionsParams opts
  where
    path = case mModelId of
      Nothing -> ["_cat", "ml", "trained_models"]
      Just modelId -> ["_cat", "ml", "trained_models", modelId]

-- | 'catTransforms' lists the transforms via
-- @GET /_cat\/transforms?format=json@. Equivalent to
-- @'catTransformsWith' 'Nothing' 'defaultCatTransformsOptions'@.
--
-- See 'catTransformsWith' for the parameterised form, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-transforms.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catTransforms :: BHRequest StatusDependant [CatTransformsRow]
catTransforms = catTransformsWith Nothing defaultCatTransformsOptions

-- | 'catTransformsWith' is the fully-parameterised form of
-- 'catTransforms'. The optional 'Text' narrows the result to a transform
-- id (wildcards accepted). Every URI parameter accepted by
-- @GET /_cat\/transforms@ — including the @from@\/@size@ pagination — is
-- exposed via 'CatTransformsOptions'. The response is a structured
-- 'CatTransformsRow' per transform, with every default column typed (the
-- natively-nullable @checkpoint_progress@\/@last_search_time@\/
-- @changes_last_detection_time@ columns decode to 'Nothing') and any
-- future columns preserved verbatim in 'ctxrOther'.
--
-- See 'catTransformsOptionsParams' for the rendering of each field, and
-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-transforms.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catTransformsWith ::
  Maybe Text ->
  CatTransformsOptions ->
  BHRequest StatusDependant [CatTransformsRow]
catTransformsWith mTransformId opts =
  get $ path `withQueries` catTransformsOptionsParams opts
  where
    path = case mTransformId of
      Nothing -> ["_cat", "transforms"]
      Just transformId -> ["_cat", "transforms", transformId]

-- | 'catHelp' shows help for the CAT APIs via @GET /_cat@, returning the
-- available cat commands as a plain-text report (the @=^.^=@ banner
-- followed by the endpoint list, the same output @curl /_cat@ produces).
-- This targets the cat root rather than the @/_cat/help@ alias because
-- the root is the only path accepted across every supported backend
-- (@/_cat/help@ 405s on ES 7.x). Unlike the other @cat*@ requests, this
-- endpoint takes no URI parameters and returns a non-JSON body, so it
-- uses 'Database.Bloodhound.Internal.Utils.Requests.getText' and yields
-- 'Text' rather than a structured row list.
--
-- See <https://www.elastic.co/guide/en/elasticsearch/reference/current/cat.html>
-- for the upstream documentation.
--
-- @since 0.26.0.0
catHelp :: BHRequest StatusDependant Text
catHelp = getText ["_cat"]

-- | 'addIndexBlock' applies an 'IndexBlock' to an index via
-- @PUT /\<index\>/_block/\<block\>@, returning 'Acknowledged' on
-- success. Useful for cluster maintenance: temporarily disabling
-- writes, reads, or metadata operations without closing the index.
--
-- See <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-blocks.html>
-- and <https://docs.opensearch.org/latest/api-reference/index-apis/block-index/>.
--
-- @since 0.26.0.0
addIndexBlock ::
  IndexName ->
  IndexBlock ->
  BHRequest StatusDependant Acknowledged
addIndexBlock indexName block =
  put [unIndexName indexName, "_block", indexBlockText block] emptyBody

-- | 'removeIndexBlock' releases an 'IndexBlock' from an index, returning
-- 'Acknowledged' on success. Counterpart to 'addIndexBlock'.
--
-- Although the dedicated @PUT /\<index\>/_block/\<block\>@ endpoint is
-- documented for applying a block, it is add-only on every supported
-- backend (ES7+ and OS1+ all reject @DELETE /\<index\>/_block/\<block\>@
-- with HTTP 405, and none recognise a @?remove=true@ parameter). The
-- canonical removal path is therefore @PUT /\<index\>/_settings@ with
-- the matching @BlocksX False@ 'UpdatableIndexSetting', which ES\/OS
-- honour even while a @metadata@ block is in effect (that block rejects
-- @GET /\<index\>/_settings@ but permits the @PUT@). This function
-- encapsulates that routing so callers see a single,
-- 'IndexBlock'-parameterised teardown.
--
-- See <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-blocks.html>
-- and <https://docs.opensearch.org/latest/api-reference/index-apis/block-index/>.
--
-- @since 0.26.0.0
removeIndexBlock ::
  IndexName ->
  IndexBlock ->
  BHRequest StatusDependant Acknowledged
removeIndexBlock indexName block =
  put [unIndexName indexName, "_settings"] (encode body)
  where
    body = toJSON (indexBlockToUpdatable block)

-- | 'updateIndexAliases' updates the server's index alias
-- table. Operations are atomic. Explained in further detail at
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-aliases.html>
--
-- >>> let src = IndexName "a-real-index"
-- >>> let aliasName = IndexName "an-alias"
-- >>> let iAlias = IndexAlias src (IndexAliasName aliasName)
-- >>> let aliasCreate = defaultIndexAliasCreate
-- >>> _ <- runBH' $ deleteIndex src
-- >>> isSuccess <$> runBH' (createIndex defaultIndexSettings src)
-- True
-- >>> runBH' $ indexExists src
-- True
-- >>> isSuccess <$> runBH' (updateIndexAliases (AddAlias iAlias aliasCreate :| []))
-- True
-- >>> runBH' $ indexExists aliasName
-- True
updateIndexAliases :: NonEmpty IndexAliasAction -> BHRequest StatusIndependant Acknowledged
updateIndexAliases = updateIndexAliasesWith defaultUpdateAliasesOptions

-- | 'updateIndexAliasesWith' is the fully-parameterised form of
-- 'updateIndexAliases'. Every URI parameter accepted by
-- @POST /_aliases@ is exposed via 'UpdateAliasesOptions'
-- (@master_timeout@, @timeout@); pass 'defaultUpdateAliasesOptions' to
-- reproduce the bare behaviour (no parameters). See
-- 'updateAliasesOptionsParams' for the rendering of each field.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-aliases.html>)
updateIndexAliasesWith ::
  UpdateAliasesOptions ->
  NonEmpty IndexAliasAction ->
  BHRequest StatusIndependant Acknowledged
updateIndexAliasesWith opts actions =
  post (["_aliases"] `withQueries` updateAliasesOptionsParams opts) (encode body)
  where
    body = object ["actions" .= toList actions]

-- | Get all aliases configured on the server.
getIndexAliases :: BHRequest StatusDependant IndexAliasesSummary
getIndexAliases =
  get ["_aliases"]

-- | Get aliases for a single source index, optionally narrowed to one
-- alias name. Hits:
--
-- * @GET /{index}/_alias\/{name}@ when the 'AliasName' is supplied;
-- * @GET /{index}/_alias@ (every alias on that index) when it is not.
--
-- The wire shape is identical to 'getIndexAliases' (the
-- @{index: {aliases: {...}}}@ envelope), but the result is wrapped in
-- 'IndexAliasesInfo' so callers can distinguish a per-index lookup
-- from a server-wide one at the type level. Like the other alias
-- getters, this is 'StatusDependant': a 404 (index missing, or the named alias
-- not attached to that index) surfaces as an 'EsError' rather than an
-- empty list — wrap with 'tryEsError' for a miss-tolerant variant.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-aliases.html>)
getIndexAlias ::
  IndexName ->
  Maybe AliasName ->
  BHRequest StatusDependant IndexAliasesInfo
getIndexAlias srcIndex mAlias =
  get endpoint
  where
    endpoint =
      case mAlias of
        Just (AliasName name) -> [unIndexName srcIndex, "_alias", unIndexName name]
        Nothing -> [unIndexName srcIndex, "_alias"]

-- | Delete a single alias name, removing it from /every/ index it is
-- currently associated with. Hits
-- @DELETE /_all\/_alias\/{name}@ — the @_all@ wildcard means an alias
-- shared by several indices is detached from all of them in one shot.
--
-- To remove an alias from one specific source index only (leaving any
-- other bindings intact), use 'deleteIndexAliasFrom'.
deleteIndexAlias :: IndexAliasName -> BHRequest StatusIndependant Acknowledged
deleteIndexAlias (IndexAliasName name) =
  delete ["_all", "_alias", unIndexName name]

-- | Remove an alias from a single source index, hitting
-- @DELETE /{index}/_alias/{name}@. Unlike 'deleteIndexAlias' (which
-- uses the @_all@ wildcard), this leaves any other index that happens
-- to publish the same alias name untouched.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-aliases.html>)
deleteIndexAliasFrom ::
  IndexName ->
  IndexAliasName ->
  BHRequest StatusIndependant Acknowledged
deleteIndexAliasFrom srcIndex (IndexAliasName name) =
  delete [unIndexName srcIndex, "_alias", unIndexName name]

-- | Add an alias to a single index, hitting
-- @PUT /{index}/_alias/{name}@. This is the non-atomic, single-action
-- variant of 'updateIndexAliases': unlike the bulk @POST /_aliases@
-- endpoint, it does not bundle multiple actions and is not atomic
-- across several indices. Pass 'defaultIndexAliasCreate' for an empty
-- alias body, or override fields such as 'aliasCreateIsWriteIndex' via
-- record update.
--
-- >>> _ <- runBH' $ createIndexAlias (IndexName "my-index") (IndexAliasName (IndexName "my-alias")) defaultIndexAliasCreate
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-aliases.html>)
createIndexAlias ::
  IndexName ->
  IndexAliasName ->
  IndexAliasCreate ->
  BHRequest StatusIndependant Acknowledged
createIndexAlias srcIndex (IndexAliasName name) body =
  put [unIndexName srcIndex, "_alias", unIndexName name] (encode body)

-- | Check whether an alias name exists anywhere in the cluster,
-- hitting @HEAD /_alias/{name}@. Returns 'True' on a 2xx response and
-- 'False' on any other status (including 404); the request never
-- throws on absence.
--
-- >>> present <- runBH' $ aliasExists (IndexAliasName (IndexName "my-alias"))
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-alias-exists.html>)
aliasExists :: IndexAliasName -> BHRequest StatusDependant Bool
aliasExists (IndexAliasName name) =
  doesExist ["_alias", unIndexName name]

-- | 'putTemplate' creates a template given an 'IndexTemplate' and a 'TemplateName'.
--  Explained in further detail at
--  <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-templates.html>
--
--  >>> let idxTpl = IndexTemplate [IndexPattern "tweet-*"] (Just (IndexSettings (ShardCount 1) (ReplicaCount 1))) [toJSON TweetMapping]
--  >>> resp <- runBH' $ putTemplate idxTpl (TemplateName "tweet-tpl")
putTemplate :: IndexTemplate -> TemplateName -> BHRequest StatusIndependant Acknowledged
putTemplate indexTemplate (TemplateName templateName) =
  put ["_template", templateName] (encode indexTemplate)

-- | 'templateExists' checks to see if a template exists.
--
--  >>> exists <- runBH' $ templateExists (TemplateName "tweet-tpl")
templateExists :: TemplateName -> BHRequest StatusDependant Bool
templateExists (TemplateName templateName) =
  doesExist ["_template", templateName]

-- | 'getTemplate' fetches /legacy/ templates (the @GET /_template@
-- endpoint). It is the read-side counterpart of 'putTemplate' (and the
-- legacy analogue of the composable 'getIndexTemplate').
--
-- * @'Nothing'@                        → @GET /_template@ returns every
--   legacy template on the cluster.
-- * @'Just' ('TemplateNamePattern' p)@ → @GET /_template\/\@p@ returns
--   the templates whose names match the pattern. @p@ may be a literal
--   name (no wildcard characters), a glob such as @"tweet-*"@, or a
--   comma-separated list; the server does the matching. A pattern that
--   matches no template yields a @404@, which (because this builder is
--   'StatusDependant') surfaces as an 'EsError' — callers that want
--   miss-tolerant lookup should use 'tryPerformBHRequest'.
--
-- The response is always a list (the wire envelope
-- @{name1: body1, name2: body2, ...}@ is peeled off by
-- 'GetTemplatesResponse' and each entry's outer key is lifted into its
-- 'tiName'), even when the pattern matches exactly one template. The
-- body of each 'TemplateInfo' is decoded into raw JSON for the
-- server-normalised @settings@ \/ @mappings@ fields; see 'TemplateInfo'
-- for why those are not typed.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-get-template.html>)
getTemplate ::
  Maybe TemplateNamePattern ->
  BHRequest StatusDependant [TemplateInfo]
getTemplate mPattern =
  getTemplatesResponseTemplates <$> get endpoint
  where
    endpoint =
      case mPattern of
        Nothing -> ["_template"]
        Just (TemplateNamePattern pat) -> ["_template", pat]

-- | 'deleteTemplate' is an HTTP DELETE and deletes a template.
--
--  >>> let idxTpl = IndexTemplate [IndexPattern "tweet-*"] (Just (IndexSettings (ShardCount 1) (ReplicaCount 1))) [toJSON TweetMapping]
--  >>> _ <- runBH' $ putTemplate idxTpl (TemplateName "tweet-tpl")
--  >>> resp <- runBH' $ deleteTemplate (TemplateName "tweet-tpl")
deleteTemplate :: TemplateName -> BHRequest StatusIndependant Acknowledged
deleteTemplate (TemplateName templateName) =
  delete ["_template", templateName]

-- | 'putIndexTemplate' creates or updates a /composable/ index template
-- (the @PUT /_index_template/{name}@ endpoint, available since
-- Elasticsearch 7.8 and in OpenSearch 2.x). This is the modern
-- replacement for the legacy 'putTemplate' (which targets
-- @PUT /_template@): composable templates can be assembled from
-- reusable component templates and carry an explicit 'ctPriority' for
-- deterministic conflict resolution.
--
-- This builder is 'StatusDependant', so a 4xx (e.g. an invalid body)
-- is surfaced as an 'EsError' rather than swallowed — matching
-- 'createIndex' and unlike the legacy 'putTemplate'. To fail when a
-- template with the given name already exists, use
-- 'putIndexTemplateWith' with @'ctoCreate' = 'Just' True@.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-put-index-template.html>)
putIndexTemplate ::
  TemplateName ->
  ComposableTemplate ->
  BHRequest StatusDependant Acknowledged
putIndexTemplate (TemplateName templateName) template =
  put ["_index_template", templateName] (encode template)

-- | Like 'putIndexTemplate' but additionally accepts
-- 'ComposableTemplateOptions' rendered as URI parameters. Use
-- 'defaultComposableTemplateOptions' to send no parameters at all, in
-- which case the emitted request is byte-for-byte identical to
-- 'putIndexTemplate'.
putIndexTemplateWith ::
  TemplateName ->
  ComposableTemplate ->
  ComposableTemplateOptions ->
  BHRequest StatusDependant Acknowledged
putIndexTemplateWith (TemplateName templateName) template opts =
  put endpoint (encode template)
  where
    endpoint =
      ["_index_template", templateName]
        `withQueries` composableTemplateOptionsParams opts

-- | 'getIndexTemplate' fetches /composable/ index templates (the
-- @GET /_index_template@ endpoint, available since Elasticsearch 7.8
-- and in OpenSearch 2.x). It is the read-side counterpart of
-- 'putIndexTemplate'.
--
-- * @'Nothing'@                       → @GET /_index_template@ returns
--   every composable template on the cluster.
-- * @'Just' ('TemplateNamePattern' p)@ → @GET /_index_template\/\@p@
--   returns the templates whose names match the pattern. @p@ may be a
--   literal name (no wildcard characters) or a glob such as
--   @"logs-*"@; the server does the matching. A pattern that matches
--   no template yields a @404@, which (because this builder is
--   'StatusDependant') surfaces as an 'EsError' — callers that want
--   miss-tolerant lookup should use 'tryPerformBHRequest'.
--
-- The response is always a list (wrapped in a
-- 'GetIndexTemplatesResponse' envelope on the wire), even when the
-- pattern matches exactly one template. The body of each
-- 'IndexTemplateInfo' is decoded into a 'ComposableTemplate'.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-get-template.html>)
getIndexTemplate ::
  Maybe TemplateNamePattern ->
  BHRequest StatusDependant [IndexTemplateInfo]
getIndexTemplate mPattern =
  getIndexTemplatesResponseTemplates <$> get endpoint
  where
    endpoint =
      case mPattern of
        Nothing -> ["_index_template"]
        Just (TemplateNamePattern pat) -> ["_index_template", pat]

-- | 'deleteIndexTemplate' deletes a /composable/ index template (the
-- @DELETE /_index_template/{name}@ endpoint, available since
-- Elasticsearch 7.8 and in OpenSearch 2.x). It is the write-side
-- counterpart of 'putIndexTemplate'.
--
-- This builder is 'StatusDependant', matching 'putIndexTemplate' and
-- 'getIndexTemplate': a 4xx (in particular a @404@ when no template
-- with the given name exists) is surfaced as an 'EsError' rather than
-- swallowed. Callers that want miss-tolerant deletion should use
-- 'tryPerformBHRequest'.
--
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-delete-index-template.html>
deleteIndexTemplate ::
  TemplateName ->
  BHRequest StatusDependant Acknowledged
deleteIndexTemplate (TemplateName templateName) =
  delete ["_index_template", templateName]

-- | 'simulateIndexTemplate' resolves a hypothetical 'ComposableTemplate'
-- against the templates already registered on the cluster (the
-- @POST /_index_template/_simulate@ endpoint, available since
-- Elasticsearch 7.8 and in OpenSearch 2.x). The server merges the
-- supplied template with any component templates it references and
-- returns the resulting @settings@\/@mappings@\/@aliases@ that would
-- be applied to a new matching index, alongside the list of /existing/
-- composable templates whose @index_patterns@ overlap the supplied
-- template's patterns.
--
-- The request body is the bare 'ComposableTemplate' (the same shape
-- used by 'putIndexTemplate' for @PUT /_index_template/{name}@); no
-- @index_template@ wrapper is required. To simulate the template that
-- /would be applied/ to a hypothetical new index name, use the
-- @POST /_index_template/_simulate_index/{name}@ variant instead —
-- tracked as sibling bead @bloodhound-04f.2.17.4@.
--
-- This builder is 'StatusDependant', matching 'putIndexTemplate': a
-- 4xx (e.g. an invalid body) is surfaced as an 'EsError' rather than
-- swallowed.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-simulate-template.html>)
simulateIndexTemplate ::
  ComposableTemplate ->
  BHRequest StatusDependant SimulatedTemplate
simulateIndexTemplate template =
  post ["_index_template", "_simulate"] (encode template)

-- | Like 'simulateIndexTemplate' but additionally accepts
-- 'ComposableTemplateOptions' rendered as URI parameters. Use
-- 'defaultComposableTemplateOptions' to send no parameters at all, in
-- which case the emitted request is byte-for-byte identical to
-- 'simulateIndexTemplate'.
--
-- The simulate endpoint documents only @master_timeout@ (and the
-- pre-7.16 alias @cluster_manager_timeout@ on OpenSearch); the
-- remaining 'ComposableTemplateOptions' fields (@create@, @cause@) are
-- accepted by this builder for symmetry with 'putIndexTemplateWith'
-- but have no effect on the server.
simulateIndexTemplateWith ::
  ComposableTemplate ->
  ComposableTemplateOptions ->
  BHRequest StatusDependant SimulatedTemplate
simulateIndexTemplateWith template opts =
  post endpoint (encode template)
  where
    endpoint =
      ["_index_template", "_simulate"]
        `withQueries` composableTemplateOptionsParams opts

-- | 'simulateIndex' resolves the composable index template(s) that
-- Elasticsearch /would/ apply to a hypothetical new index with the given
-- name — the @POST /_index_template/_simulate_index/{name}@ endpoint,
-- available since Elasticsearch 7.8 and in OpenSearch 2.x. Returns the
-- merged @settings@\/@mappings@\/@aliases@ plus any lower-priority
-- templates whose @index_patterns@ also match (the @overlapping@ field
-- of 'SimulatedTemplate'). Read-only and side-effect-free: the index is
-- not created.
--
-- This is the index-name-driven counterpart of 'simulateIndexTemplate'
-- (which simulates a supplied 'ComposableTemplate' body against the
-- cluster's existing templates). Use 'simulateIndex' when you want to
-- know "what would index @logs-2026-06-21@ actually look like?";
-- use 'simulateIndexTemplate' when you want to know "what would this
-- hypothetical template I haven't PUT yet produce?".
--
-- The request is sent with no body: the index name carries all the
-- input and the server resolves against the already-registered
-- templates. (The endpoint also accepts an override body carrying an
-- @index_patterns@ field to simulate a hypothetical template on the
-- fly; that variant is not surfaced here because it overlaps with
-- 'simulateIndexTemplate' — add a follow-up if a caller needs to mix
-- both axes.)
--
-- This builder is 'StatusDependant', matching 'simulateIndexTemplate'.
--
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-simulate-index-api.html>
simulateIndex ::
  IndexName ->
  BHRequest StatusDependant SimulatedTemplate
simulateIndex indexName =
  post ["_index_template", "_simulate_index", unIndexName indexName] emptyBody

-- | Like 'simulateIndex' but additionally accepts
-- 'ComposableTemplateOptions' rendered as URI parameters. Use
-- 'defaultComposableTemplateOptions' to send no parameters at all, in
-- which case the emitted request is byte-for-byte identical to
-- 'simulateIndex'.
--
-- The simulate-index endpoint documents only @master_timeout@ (and the
-- pre-7.16 alias @cluster_manager_timeout@ on OpenSearch); the remaining
-- 'ComposableTemplateOptions' fields (@create@, @cause@) are accepted by
-- this builder for symmetry with 'simulateIndexTemplateWith' but have no
-- effect on the server.
simulateIndexWith ::
  IndexName ->
  ComposableTemplateOptions ->
  BHRequest StatusDependant SimulatedTemplate
simulateIndexWith indexName opts =
  post endpoint emptyBody
  where
    endpoint =
      ["_index_template", "_simulate_index", unIndexName indexName]
        `withQueries` composableTemplateOptionsParams opts

-- | 'putComponentTemplate' creates or updates a reusable /component/
-- template (the @PUT /_component_template/{name}@ endpoint, available
-- since Elasticsearch 7.8 and in OpenSearch 2.x). Component templates
-- are building blocks composed into composable index templates (see
-- 'ComposableTemplate'\'s @composed_of@ field); they have no
-- @index_patterns@ of their own.
--
-- This builder is 'StatusDependant', matching 'putIndexTemplate': a 4xx
-- (e.g. an invalid body) is surfaced as an 'EsError' rather than
-- swallowed. To fail when a component template with the given name
-- already exists, issue the request with a @create=true@ query
-- parameter (not modelled here — use the underlying 'put' if needed).
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-component-template.html>)
putComponentTemplate ::
  TemplateName ->
  ComponentTemplate ->
  BHRequest StatusDependant Acknowledged
putComponentTemplate (TemplateName templateName) template =
  put ["_component_template", templateName] (encode template)

-- | 'deleteComponentTemplate' deletes a reusable /component/ template
-- (the @DELETE /_component_template/{name}@ endpoint, available since
-- Elasticsearch 7.8 and in OpenSearch 2.x). It is the write-side
-- counterpart of 'putComponentTemplate'.
--
-- This builder is 'StatusDependant', matching 'putComponentTemplate' and
-- 'deleteIndexTemplate': a 4xx (in particular a @404@ when no template
-- with the given name exists) is surfaced as an 'EsError' rather than
-- swallowed. Callers that want miss-tolerant deletion should use
-- 'tryPerformBHRequest'.
--
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/delete-component-template.html>
deleteComponentTemplate ::
  TemplateName ->
  BHRequest StatusDependant Acknowledged
deleteComponentTemplate (TemplateName templateName) =
  delete ["_component_template", templateName]

-- | 'getComponentTemplate' fetches reusable /component/ templates (the
-- @GET /_component_template@ endpoint, available since Elasticsearch
-- 7.8 and in OpenSearch 2.x). It is the read-side counterpart of
-- 'putComponentTemplate'.
--
-- * @'Nothing'@                       → @GET /_component_template@
--   returns every component template on the cluster.
-- * @'Just' ('TemplateNamePattern' p)@ → @GET /_component_template\/\@p@
--   returns the templates whose names match the pattern. @p@ may be a
--   literal name (no wildcard characters) or a glob such as
--   @"logs-*"@; the server does the matching. A pattern that matches
--   no template yields a @404@, which (because this builder is
--   'StatusDependant') surfaces as an 'EsError' — callers that want
--   miss-tolerant lookup should use 'tryPerformBHRequest'.
--
-- The response is always a list (wrapped in a
-- 'GetComponentTemplatesResponse' envelope on the wire), even when the
-- pattern matches exactly one template. The body of each
-- 'ComponentTemplateInfo' is decoded into a 'ComponentTemplate'.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/getting-component-templates.html>)
getComponentTemplate ::
  Maybe TemplateNamePattern ->
  BHRequest StatusDependant [ComponentTemplateInfo]
getComponentTemplate mPattern =
  getComponentTemplatesResponseTemplates <$> get endpoint
  where
    endpoint =
      case mPattern of
        Nothing -> ["_component_template"]
        Just (TemplateNamePattern pat) -> ["_component_template", pat]

-- | 'putMapping' is an HTTP PUT and has upsert semantics. Mappings are schemas
-- for documents in indexes.
--
-- Equivalent to @'putMappingWith' 'defaultPutMappingOptions'@. Use the
-- @With@ variant to pass @allow_no_indices@, @expand_wildcards@,
-- @ignore_unavailable@, @master_timeout@, @timeout@ or
-- @write_index_only@.
--
-- >>> _ <- runBH' $ createIndex defaultIndexSettings testIndex
-- >>> resp <- runBH' $ putMapping testIndex TweetMapping
-- >>> print resp
-- Response {responseStatus = Status {statusCode = 200, statusMessage = "OK"}, responseVersion = HTTP/1.1, responseHeaders = [("content-type","application/json; charset=UTF-8"),("content-encoding","gzip"),("transfer-encoding","chunked")], responseBody = "{\"acknowledged\":true}", responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose}
putMapping :: (FromJSON r, ToJSON a) => IndexName -> a -> BHRequest StatusDependant r
putMapping indexName mapping =
  putMappingWith indexName mapping defaultPutMappingOptions

-- | 'putMappingWith' is the fully-parameterised form of 'putMapping'.
-- Every URI parameter accepted by @PUT /{index}/_mapping@ is exposed via
-- 'PutMappingOptions'. 'defaultPutMappingOptions' makes this
-- byte-for-byte identical to 'putMapping'.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-put-mapping.html>)
putMappingWith ::
  (FromJSON r, ToJSON a) =>
  IndexName ->
  a ->
  PutMappingOptions ->
  BHRequest StatusDependant r
putMappingWith indexName mapping opts =
  put endpoint (encode mapping)
  where
    endpoint =
      [unIndexName indexName, "_mapping"]
        `withQueries` putMappingOptionsParams opts

-- | 'getMapping' fetches the mapping for a given index. Wraps the
-- @GET \/\<index\>\/_mapping@ endpoint
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-get-mapping.html>).
--
-- The response is generic over any 'FromJSON' @r@, mirroring
-- 'putMapping'. Decode into a 'Value' for an untyped view of
-- the mapping (the response is shaped @{\<index\>: {mappings: {...}}}@),
-- or into a custom type. Use 'getIndex' to fetch aliases and settings
-- alongside the mappings in a single request.
getMapping :: (FromJSON r) => IndexName -> BHRequest StatusDependant r
getMapping indexName =
  get [unIndexName indexName, "_mapping"]

-- | 'getFieldMapping' fetches the mapping for specific fields of a given
-- index. Wraps the @GET \/\<index\>\/_mapping\/field\/\<fields\>@ endpoint
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-get-field-mapping.html>).
--
-- The response is generic over any 'FromJSON' @r', mirroring 'getMapping'.
-- Decode into a 'Value' for an untyped view, or into
-- 'FieldMappingResponse' for a structured one. The field list is joined
-- with commas, matching the ES wire format. Pass an empty list to let the
-- server reject the request.
--
-- Field names are interpolated verbatim into the URL path (consistent with
-- the rest of the library, which does not percent-encode path segments).
-- As a consequence, a field name containing @,@ cannot be expressed on the
-- wire (the comma is the list separator) and must be requested in a
-- separate single-field call; characters such as @\/@ or @?@ will corrupt
-- the URL. Wildcards like @"*"@ or @"user*"@ work as expected.
getFieldMapping ::
  (FromJSON r) =>
  IndexName ->
  [FieldName] ->
  BHRequest StatusDependant r
getFieldMapping indexName fields =
  get [unIndexName indexName, "_mapping", "field", renderedFields]
  where
    renderedFields = T.intercalate "," (map unFieldName fields)

versionCtlParams :: IndexDocumentSettings -> [(Text, Maybe Text)]
versionCtlParams cfg =
  case idsVersionControl cfg of
    NoVersionControl -> []
    InternalVersion v -> versionParams v "internal"
    ExternalGTE (ExternalDocVersion v) -> versionParams v "external_gte"
    ForceVersion (ExternalDocVersion v) -> versionParams v "force"
  where
    vt = showText . docVersionNumber
    versionParams :: DocVersion -> Text -> [(Text, Maybe Text)]
    versionParams v t =
      [ ("version", Just $ vt v),
        ("version_type", Just t)
      ]

-- | 'indexDocument' is the primary way to save a single document in
--  Elasticsearch. The document itself is simply something we can
--  convert into a JSON 'Value'. The 'DocId' will function as the
--  primary key for the document. You are encouraged to generate
--  your own id's and not rely on Elasticsearch's automatic id
--  generation. Read more about it here:
--  https://github.com/bitemyapp/bloodhound/issues/107
--
-- >>> resp <- runBH' $ indexDocument testIndex defaultIndexDocumentSettings exampleTweet (DocId "1")
-- >>> print resp
-- Response {responseStatus = Status {statusCode = 200, statusMessage = "OK"}, responseVersion = HTTP/1.1, responseHeaders = [("content-type","application/json; charset=UTF-8"),("content-encoding","gzip"),("content-length","152")], responseBody = "{\"_index\":\"twitter\",\"_type\":\"_doc\",\"_id\":\"1\",\"_version\":2,\"result\":\"updated\",\"_shards\":{\"total\":1,\"successful\":1,\"failed\":0},\"_seq_no\":1,\"_primary_term\":1}", responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose}
indexDocument ::
  (ToJSON doc) =>
  IndexName ->
  IndexDocumentSettings ->
  doc ->
  DocId ->
  BHRequest StatusDependant IndexedDocument
indexDocument indexName cfg document (DocId docId) =
  post endpoint (encode body)
  where
    endpoint = [unIndexName indexName, "_doc", docId] `withQueries` indexQueryString cfg (DocId docId)
    body = encodeDocument cfg document

-- | 'updateDocument' provides a way to perform an partial update of a
-- an already indexed document.
--
-- Sends the @{\"doc\": …}@ body shape only. For script-driven updates,
-- upserts, @doc_as_upsert@ or @scripted_upsert@, use 'updateDocumentWith'
-- with an 'UpdateBody' constructed via 'mkUpdateScript' (or the
-- 'UpdateScript' constructor directly).
updateDocument ::
  (ToJSON patch) =>
  IndexName ->
  IndexDocumentSettings ->
  patch ->
  DocId ->
  BHRequest StatusDependant IndexedDocument
updateDocument indexName cfg patch (DocId docId) =
  updateDocumentWith indexName cfg (mkUpdateDoc (encodeDocument cfg patch)) (DocId docId)

-- | Like 'updateDocument' but accepts the full 'UpdateBody' union,
-- supporting both @{\"doc\": …}@ (partial-document) and
-- @{\"script\": …}@ (script-driven, with optional upsert and
-- @scripted_upsert@) wire shapes. The 'IndexDocumentSettings' argument
-- still supplies the URI-level parameters (@if_seq_no@, @refresh@,
-- @op_type@, …) shared with the Index API.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-update.html docs-update>.
updateDocumentWith ::
  IndexName ->
  IndexDocumentSettings ->
  UpdateBody ->
  DocId ->
  BHRequest StatusDependant IndexedDocument
updateDocumentWith indexName cfg body (DocId docId) =
  post endpoint (encode body)
  where
    endpoint = [unIndexName indexName, "_update", docId] `withQueries` indexQueryString cfg (DocId docId)

updateByQuery ::
  (FromJSON a) =>
  IndexName ->
  Query ->
  Maybe Script ->
  BHRequest StatusDependant a
updateByQuery = updateByQueryWith defaultByQueryOptions

-- | Like 'updateByQuery' but accepts 'ByQueryOptions' carrying the
-- URI-level parameters documented at
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-update-by-query.html#docs-update-by-query-api-query-params docs-update-by-query>.
-- 'defaultByQueryOptions' makes this byte-for-byte equivalent to
-- 'updateByQuery'.
updateByQueryWith ::
  (FromJSON a) =>
  ByQueryOptions ->
  IndexName ->
  Query ->
  Maybe Script ->
  BHRequest StatusDependant a
updateByQueryWith opts indexName q mScript =
  post endpoint (encode body)
  where
    endpoint = [unIndexName indexName, "_update_by_query"] `withQueries` byQueryOptionsParams opts
    body = Object $ ("query" .= q) <> scriptObject
    scriptObject :: X.KeyMap Value
    scriptObject = case toJSON mScript of
      Null -> mempty
      Object o -> o
      x -> "script" .= x

-- | 'rethrottleUpdateByQuery' changes the maximum documents-per-second
-- rate of an in-progress asynchronous @update_by_query@ task. Maps to
-- @POST /_update_by_query/{task_id}/_rethrottle?requests_per_second=<n>@.
-- The 'TaskNodeId' is the composite @nodeId:localId@ identifier returned
-- by an asynchronous 'updateByQueryWith' (call it with
-- @bqoWaitForCompletion = Just False@) or observed via 'listTasks' \/
-- 'getTask'.
--
-- The response uses the same nodes-grouped shape as 'cancelTask'
-- (a 'TaskListResponse' listing the task(s) whose rate was changed),
-- /not/ an 'Acknowledged' — the ES wire format is a task list even
-- though the operation is conceptually an acknowledgement. An empty
-- node list typically means the task had already finished by the time
-- the request reached the server.
--
-- Rethrottling that speeds the task up takes effect immediately;
-- rethrottling that slows it down takes effect after the current
-- batch completes (to prevent scroll timeouts).
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-update-by-query.html#docs-update-by-query-rethrottle-api docs-update-by-query-rethrottle-api>.
rethrottleUpdateByQuery ::
  TaskNodeId ->
  RethrottleRate ->
  BHRequest StatusDependant TaskListResponse
rethrottleUpdateByQuery (TaskNodeId task) rate =
  post endpoint emptyBody
  where
    endpoint =
      ["_update_by_query", task, "_rethrottle"]
        `withQueries` [("requests_per_second", Just (renderRethrottleRate rate))]

{-  From ES docs:
      Parent and child documents must be indexed on the same shard.
      This means that the same routing value needs to be provided when getting, deleting, or updating a child document.

    Parent/Child support in Bloodhound requires MUCH more love.
    To work it around for now (and to support the existing unit test) we route "parent" documents to their "_id"
    (which is the default strategy for the ES), and route all child documents to their parens' "_id"

    However, it may not be flexible enough for some corner cases.

    Buld operations are completely unaware of "routing" and are probably broken in that matter.
    Or perhaps they always were, because the old "_parent" would also have this requirement.
-}
indexQueryString :: IndexDocumentSettings -> DocId -> [(Text, Maybe Text)]
indexQueryString cfg (DocId docId) =
  versionCtlParams cfg
    <> routeParams
    <> opTypeParams
    <> pipelineParams
    <> refreshParams
    <> waitForActiveShardsParams
    <> ifSeqNoParams
    <> requireAliasParams
    <> timeoutParams
  where
    routeParams = case idsJoinRelation cfg of
      Nothing -> []
      Just (ParentDocument _ _) -> [("routing", Just docId)]
      Just (ChildDocument _ _ (DocId pid)) -> [("routing", Just pid)]
    opTypeParams = case idsOpType cfg of
      Just opType -> [("op_type", Just (renderOpType opType))]
      Nothing -> []
    pipelineParams = case idsPipeline cfg of
      Just pipeline -> [("pipeline", Just pipeline)]
      Nothing -> []
    refreshParams = case idsRefresh cfg of
      Just policy -> [("refresh", Just (renderRefreshPolicy policy))]
      Nothing -> []
    waitForActiveShardsParams = case idsWaitForActiveShards cfg of
      Just count -> [("wait_for_active_shards", Just (renderActiveShardCount count))]
      Nothing -> []
    requireAliasParams = case idsRequireAlias cfg of
      Just flag -> [("require_alias", Just (boolText flag))]
      Nothing -> []
    timeoutParams = case idsTimeout cfg of
      Just d -> [("timeout", Just (renderDuration d))]
      Nothing -> []
    renderDuration (u, n) = showText n <> timeUnitsSuffix u
    -- Sequence number and primary term: ES requires both to be set
    -- together for optimistic concurrency control. If the caller sets
    -- only one, we still emit it — the server will then respond with
    -- HTTP 400, which is much louder than silently dropping the check
    -- (the previous behaviour would have indexed without OCC, hiding
    -- the caller's mistake). See
    -- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-index_.html#docs-index-api-if-seq-no>.
    ifSeqNoParams = case (idsIfSeqNo cfg, idsIfPrimaryTerm cfg) of
      (Just seqNo, Just primaryTerm) ->
        [ ("if_seq_no", Just (showText seqNo)),
          ("if_primary_term", Just (showText primaryTerm))
        ]
      (Just seqNo, Nothing) ->
        [ ("if_seq_no", Just (showText seqNo))
        ]
      (Nothing, Just primaryTerm) ->
        [ ("if_primary_term", Just (showText primaryTerm))
        ]
      (Nothing, Nothing) -> []
    boolText :: Bool -> Text
    boolText True = "true"
    boolText False = "false"

encodeDocument :: (ToJSON doc) => IndexDocumentSettings -> doc -> Value
encodeDocument cfg document =
  case idsJoinRelation cfg of
    Nothing -> toJSON document
    Just (ParentDocument (FieldName field) name) ->
      mergeObjects (toJSON document) (object [fromText field .= name])
    Just (ChildDocument (FieldName field) name parent) ->
      mergeObjects (toJSON document) (object [fromText field .= object ["name" .= name, "parent" .= parent]])
  where
    mergeObjects (Object a) (Object b) = Object (a <> b)
    mergeObjects _ _ = error "Impossible happened: both document body and join parameters must be objects"

-- | 'deleteDocument' is the primary way to delete a single document.
--
-- >>> _ <- runBH' $ deleteDocument testIndex (DocId "1")
deleteDocument :: IndexName -> DocId -> BHRequest StatusDependant IndexedDocument
deleteDocument = deleteDocumentWith defaultDeleteDocumentOptions

-- | Like 'deleteDocument' but accepts 'DeleteDocumentOptions' carrying
-- the URI-level parameters documented at
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-delete.html#docs-delete-api-query-params docs-delete>
-- (@wait_for_active_shards@, @refresh@, @routing@, @timeout@,
-- @version@, @version_type@, @if_seq_no@, @if_primary_term@).
-- 'defaultDeleteDocumentOptions' makes this byte-for-byte equivalent
-- to 'deleteDocument'.
deleteDocumentWith ::
  DeleteDocumentOptions ->
  IndexName ->
  DocId ->
  BHRequest StatusDependant IndexedDocument
deleteDocumentWith opts indexName (DocId docId) =
  delete ([unIndexName indexName, "_doc", docId] `withQueries` deleteDocumentOptionsParams opts)

-- | 'deleteByQuery' performs a deletion on every document that matches a query.
--
-- >>> let query = TermQuery (Term "user" "bitemyapp") Nothing
-- >>> _ <- runBH' $ deleteByQuery testIndex query
deleteByQuery ::
  (FromJSON a) =>
  IndexName ->
  Query ->
  BHRequest StatusDependant a
deleteByQuery = deleteByQueryWith defaultByQueryOptions

-- | Like 'deleteByQuery' but accepts 'ByQueryOptions' carrying the
-- URI-level parameters documented at
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-delete-by-query.html#docs-delete-by-query-api-query-params docs-delete-by-query>.
-- 'defaultByQueryOptions' makes this byte-for-byte equivalent to
-- 'deleteByQuery'. Like 'updateByQueryWith' the response type is
-- polymorphic: callers that pass @bqoWaitForCompletion = Just False@
-- should instantiate it at 'TaskNodeId' to receive the background
-- task's identifier (e.g. for 'rethrottleDeleteByQuery').
deleteByQueryWith ::
  (FromJSON a) =>
  ByQueryOptions ->
  IndexName ->
  Query ->
  BHRequest StatusDependant a
deleteByQueryWith opts indexName query =
  post endpoint (encode body)
  where
    endpoint = [unIndexName indexName, "_delete_by_query"] `withQueries` byQueryOptionsParams opts
    body = object ["query" .= query]

-- | 'rethrottleDeleteByQuery' changes the maximum documents-per-second
-- rate of an in-progress asynchronous @delete_by_query@ task. Maps to
-- @POST /_delete_by_query/{task_id}/_rethrottle?requests_per_second=<n>@.
-- The 'TaskNodeId' is the composite @nodeId:localId@ identifier returned
-- by an asynchronous 'deleteByQueryWith' (call it with
-- @bqoWaitForCompletion = Just False@) or observed via 'listTasks' \/
-- 'getTask'.
--
-- The response uses the same nodes-grouped shape as 'cancelTask'
-- (a 'TaskListResponse' listing the task(s) whose rate was changed),
-- /not/ an 'Acknowledged' — the ES wire format is a task list even
-- though the operation is conceptually an acknowledgement. An empty
-- node list typically means the task had already finished by the time
-- the request reached the server.
--
-- Rethrottling that speeds the task up takes effect immediately;
-- rethrottling that slows it down takes effect after the current
-- batch completes (to prevent scroll timeouts).
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-delete-by-query.html#docs-delete-by-query-rethrottle-api docs-delete-by-query-rethrottle-api>.
rethrottleDeleteByQuery ::
  TaskNodeId ->
  RethrottleRate ->
  BHRequest StatusDependant TaskListResponse
rethrottleDeleteByQuery (TaskNodeId task) rate =
  post endpoint emptyBody
  where
    endpoint =
      ["_delete_by_query", task, "_rethrottle"]
        `withQueries` [("requests_per_second", Just (renderRethrottleRate rate))]

-- | 'bulk' uses
--   <http://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-bulk.html Elasticsearch's bulk API>
--   to perform bulk operations. The 'BulkOperation' data type encodes the
--   index\/update\/delete\/create operations. You pass a 'V.Vector' of 'BulkOperation's
--   and a 'Server' to 'bulk' in order to send those operations up to your Elasticsearch
--   server to be performed. I changed from [BulkOperation] to a Vector due to memory overhead.
--
--   Uses 'defaultBulkSettings' (no URI parameters). For control over the
--   @refresh@, @routing@, @pipeline@, @wait_for_active_shards@, etc.
--   parameters, use 'bulkWith'.
--
-- >>> let stream = V.fromList [BulkIndex testIndex (DocId "2") (toJSON (BulkTest "blah")) []]
-- >>> _ <- runBH' $ bulk stream
-- >>> _ <- runBH' $ refreshIndex testIndex
bulk ::
  (ParseBHResponse contextualized) =>
  V.Vector BulkOperation ->
  BHRequest contextualized BulkResponse
bulk =
  bulkWith defaultBulkSettings

-- | Like 'bulk' but accepts a 'BulkSettings' record carrying the URI-level
-- parameters of the @POST /_bulk@ endpoint (@refresh@, @timeout@,
-- @wait_for_active_shards@, @routing@, @_source*@, @pipeline@,
-- @require_alias@). 'defaultBulkSettings' makes this byte-for-byte
-- equivalent to 'bulk'.
--
-- Per-operation metadata (e.g. @_routing@, @_if_seq_no@) is supplied
-- via the final @[UpsertActionMetadata]@ argument of each
-- 'BulkOperation' constructor.
bulkWith ::
  (ParseBHResponse contextualized) =>
  BulkSettings ->
  V.Vector BulkOperation ->
  BHRequest contextualized BulkResponse
bulkWith opts =
  post (["_bulk"] `withQueries` bulkSettingsParams opts) . encodeBulkOperations

-- | 'encodeBulkOperations' is a convenience function for dumping a vector of 'BulkOperation'
--  into an 'L.ByteString'
--
-- >>> let bulkOps = V.fromList [BulkIndex testIndex (DocId "2") (toJSON (BulkTest "blah")) []]
-- >>> encodeBulkOperations bulkOps
-- "\n{\"index\":{\"_id\":\"2\",\"_index\":\"twitter\"}}\n{\"name\":\"blah\"}\n"
encodeBulkOperations :: V.Vector BulkOperation -> L.ByteString
encodeBulkOperations stream = collapsed
  where
    blobs =
      fmap encodeBulkOperation stream
    mashedTaters =
      mash (mempty :: Builder) blobs
    collapsed =
      toLazyByteString $ mappend mashedTaters (byteString "\n")
    mash :: Builder -> V.Vector L.ByteString -> Builder
    mash = V.foldl' (\b x -> b <> byteString "\n" <> lazyByteString x)

mkBulkStreamValueWithMeta :: [UpsertActionMetadata] -> Text -> IndexName -> Text -> Value
mkBulkStreamValueWithMeta meta operation indexName docId =
  object
    [ fromText operation
        .= object
          ( [ "_index" .= indexName,
              "_id" .= docId
            ]
              <> (buildUpsertActionMetadata <$> meta)
          )
    ]

mkBulkStreamValueAutoWithMeta :: [UpsertActionMetadata] -> Text -> IndexName -> Value
mkBulkStreamValueAutoWithMeta meta operation indexName =
  object
    [ fromText operation
        .= object
          ( ["_index" .= indexName]
              <> (buildUpsertActionMetadata <$> meta)
          )
    ]

-- | 'encodeBulkOperation' is a convenience function for dumping a single 'BulkOperation'
--  into an 'L.ByteString'
--
-- >>> let bulkOp = BulkIndex testIndex (DocId "2") (toJSON (BulkTest "blah")) []
-- >>> encodeBulkOperation bulkOp
-- "{\"index\":{\"_id\":\"2\",\"_index\":\"twitter\"}}\n{\"name\":\"blah\"}"
encodeBulkOperation :: BulkOperation -> L.ByteString
encodeBulkOperation (BulkIndex indexName (DocId docId) value meta) = blob
  where
    metadata = mkBulkStreamValueWithMeta meta "index" indexName docId
    blob = encode metadata `mappend` "\n" `mappend` encode value
encodeBulkOperation (BulkIndexAuto indexName value meta) = blob
  where
    metadata = mkBulkStreamValueAutoWithMeta meta "index" indexName
    blob = encode metadata `mappend` "\n" `mappend` encode value
encodeBulkOperation (BulkIndexEncodingAuto indexName encoding meta) = toLazyByteString blob
  where
    metadata = toEncoding (mkBulkStreamValueAutoWithMeta meta "index" indexName)
    blob = fromEncoding metadata <> "\n" <> fromEncoding encoding
encodeBulkOperation (BulkCreate indexName (DocId docId) value meta) = blob
  where
    metadata = mkBulkStreamValueWithMeta meta "create" indexName docId
    blob = encode metadata `mappend` "\n" `mappend` encode value
encodeBulkOperation (BulkDelete indexName (DocId docId) meta) = blob
  where
    metadata = mkBulkStreamValueWithMeta meta "delete" indexName docId
    blob = encode metadata
encodeBulkOperation (BulkUpdate indexName (DocId docId) value meta) = blob
  where
    metadata = mkBulkStreamValueWithMeta meta "update" indexName docId
    doc = object ["doc" .= value]
    blob = encode metadata `mappend` "\n" `mappend` encode doc
encodeBulkOperation
  ( BulkUpsert
      indexName
      (DocId docId)
      payload
      meta
    ) = blob
    where
      metadata = mkBulkStreamValueWithMeta meta "update" indexName docId
      blob = encode metadata <> "\n" <> encode doc
      doc = case payload of
        UpsertDoc value -> object ["doc" .= value, "doc_as_upsert" .= True]
        UpsertScript scriptedUpsert script value ->
          let scup = if scriptedUpsert then ["scripted_upsert" .= True] else []
              upsert = ["upsert" .= value]
           in case (object (scup <> upsert), toJSON script) of
                (Object obj, Object jscript) -> Object $ jscript <> obj
                _ -> error "Impossible happened: serialising Script to Json should always be Object"
encodeBulkOperation (BulkCreateEncoding indexName (DocId docId) encoding meta) =
  toLazyByteString blob
  where
    metadata = toEncoding (mkBulkStreamValueWithMeta meta "create" indexName docId)
    blob = fromEncoding metadata <> "\n" <> fromEncoding encoding

-- | 'getDocument' is a straight-forward way to fetch a single document from
--  Elasticsearch using a 'Server', 'IndexName', and a 'DocId'.
--  The 'DocId' is the primary key for your Elasticsearch document.
--
-- >>> yourDoc <- runBH' $ getDocument testIndex (DocId "1")
getDocument :: (FromJSON a) => IndexName -> DocId -> BHRequest StatusIndependant (EsResult a)
getDocument = getDocumentWith defaultGetDocumentOptions

-- | Like 'getDocument' but accepts 'GetDocumentOptions' carrying the
-- URI-level parameters (@_source@, @_source_includes@\/@_source_excludes@,
-- @stored_fields@, @preference@, @realtime@, @refresh@, @routing@,
-- @version@, @version_type@). 'defaultGetDocumentOptions' makes this
-- byte-for-byte equivalent to 'getDocument'.
getDocumentWith ::
  (FromJSON a) =>
  GetDocumentOptions ->
  IndexName ->
  DocId ->
  BHRequest StatusIndependant (EsResult a)
getDocumentWith opts indexName (DocId docId) =
  get ([unIndexName indexName, "_doc", docId] `withQueries` getDocumentOptionsParams opts)

-- | 'getDocumentSource' fetches only the @_source@ of a document (no
-- metadata envelope). Maps to
-- @GET \/{index}\/_source\/{id}@. The response body is the raw
-- @_source@ JSON the server stores for the document, decoded into
-- whichever 'FromJSON' type the caller picks.
--
-- The source-only endpoint is served at @\/{index}\/_source\/{id}@ on
-- every supported backend (ES 7, 8, 9 and OpenSearch). ES 7 also
-- accepted the legacy @\/{index}\/_doc\/{id}\/_source@ path, but ES
-- 8.19+ and 9.x reject it, so the cross-version-safe path is used
-- here.
--
-- A missing document (HTTP 404) surfaces as @'Right' 'Nothing'@ when
-- run via 'Database.Bloodhound.Client.Cluster.tryPerformBHRequest',
-- and as 'Nothing' from
-- 'Database.Bloodhound.Common.Client.getDocumentSource'. Other
-- non-2xx responses are decoded as an 'EsError' as usual. The
-- source-only endpoint returns no @{\"found\": false}@ envelope
-- (unlike the full GET API), so 404 is the only signal for a missing
-- document; we therefore treat it as the expected \"no document\"
-- outcome rather than an error.
--
-- >>> yourSource <- runBH' $ getDocumentSource testIndex (DocId "1")
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-get.html#get-source-api>
-- and
-- <https://docs.opensearch.org/latest/api-reference/document-apis/get-documents/>.
getDocumentSource ::
  (FromJSON a) =>
  IndexName ->
  DocId ->
  BHRequest StatusDependant (Maybe a)
getDocumentSource = getDocumentSourceWith defaultGetDocumentSourceOptions

-- | Like 'getDocumentSource' but accepts 'GetDocumentSourceOptions'
-- carrying the source-filtering and routing parameters that apply to
-- the source-only endpoint
-- (@"_source_includes"@, @"_source_excludes"@, @preference@,
-- @realtime@, @refresh@, @routing@, @version@, @version_type@).
getDocumentSourceWith ::
  (FromJSON a) =>
  GetDocumentSourceOptions ->
  IndexName ->
  DocId ->
  BHRequest StatusDependant (Maybe a)
getDocumentSourceWith opts indexName (DocId docId) =
  BHRequest
    { bhRequestMethod = NHTM.methodGet,
      bhRequestEndpoint =
        [unIndexName indexName, "_source", docId]
          `withQueries` getDocumentSourceOptionsParams opts,
      bhRequestBody = Nothing,
      bhRequestQueryStrings = [],
      bhRequestParser = getDocumentSourceParser
    }

-- | Parser used by 'getDocumentSourceWith'. The ES\/OS source-only
-- endpoint returns the raw @_source@ JSON on 2xx and 404 on missing
-- documents (there is no @{\"found\": false}@ envelope as the full
-- GET API produces). We therefore branch on the HTTP status code:
--
--   * 404 → @'Right' 'Nothing'@ — the expected \"no document\" case.
--   * 2xx → body decoded as /a/ and wrapped in 'Just'.
--   * Other non-2xx → routed through 'parseEsResponse', surfacing as
--     'Left' 'EsError'.
getDocumentSourceParser ::
  forall a parsingContext.
  (FromJSON a) =>
  BHResponse parsingContext (Maybe a) ->
  Either EsProtocolException (ParsedEsResponse (Maybe a))
getDocumentSourceParser resp
  | statusCodeIs (404, 404) resp = return (Right Nothing)
  -- Lift 'Just' into 'ParsedEsResponse' (= @Either EsError@): a
  -- decoded source becomes @Right (Just a)@, a server-reported
  -- 'EsError' is preserved as @Left e@.
  | otherwise = fmap (fmap Just) (parseEsResponse (coerce @(BHResponse parsingContext (Maybe a)) @(BHResponse parsingContext a) resp))

getDocuments ::
  (FromJSON a) =>
  IndexName ->
  [DocId] ->
  BHRequest StatusIndependant (MultiGetResponse a)
getDocuments = getDocumentsWith defaultMultiGetOptions

-- | Like 'getDocuments' but accepts 'MultiGetOptions' applied at the
-- URI level to every requested document. For per-document source
-- filtering, build the 'MultiGet' body directly (with
-- 'multiGetDocSource'\/'multiGetDocStoredFields' on each 'MultiGetDoc')
-- and use 'getDocumentsMultiWith'.
getDocumentsWith ::
  (FromJSON a) =>
  MultiGetOptions ->
  IndexName ->
  [DocId] ->
  BHRequest StatusIndependant (MultiGetResponse a)
getDocumentsWith opts indexName docIds =
  post endpoint (encode body)
  where
    endpoint = [unIndexName indexName, "_mget"] `withQueries` multiGetOptionsParams opts
    body = MultiGet $ map mkMultiGetDoc docIds

getDocumentsMulti ::
  (FromJSON a) =>
  MultiGet ->
  BHRequest StatusIndependant (MultiGetResponse a)
getDocumentsMulti = getDocumentsMultiWith defaultMultiGetOptions

-- | Like 'getDocumentsMulti' but accepts 'MultiGetOptions' applied at
-- the URI level. Per-document @_source@\/@stored_fields@ filtering is
-- carried by the 'MultiGetDoc' records inside the 'MultiGet' body.
getDocumentsMultiWith ::
  (FromJSON a) =>
  MultiGetOptions ->
  MultiGet ->
  BHRequest StatusIndependant (MultiGetResponse a)
getDocumentsMultiWith opts body =
  post (["_mget"] `withQueries` multiGetOptionsParams opts) (encode body)

-- | 'getTermVectors' returns information and statistics about terms
-- in the fields of a particular document. Maps to
-- @POST \/{index}\/_termvectors\/{id}@. The 'TermVectorsRequest'
-- body carries the optional flags (@fields@, @offsets@,
-- @positions@, @term_statistics@, @field_statistics@, @payload@,
-- @filter@); pass 'defaultTermVectorsRequest' for the
-- parameterless form.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-termvectors.html>
-- and
-- <https://docs.opensearch.org/latest/api-reference/document-apis/term-vectors/>.
getTermVectors ::
  IndexName ->
  DocId ->
  TermVectorsRequest ->
  BHRequest StatusDependant TermVectors
getTermVectors = getTermVectorsWith defaultTermVectorsOptions

-- | Like 'getTermVectors' but accepts 'TermVectorsOptions' carrying
-- the URI-level parameters documented at
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-termvectors.html docs-termvectors>
-- (@preference@, @realtime@, @routing@, @version@, @version_type@).
-- 'defaultTermVectorsOptions' makes the wire output byte-identical to
-- 'getTermVectors'.
getTermVectorsWith ::
  TermVectorsOptions ->
  IndexName ->
  DocId ->
  TermVectorsRequest ->
  BHRequest StatusDependant TermVectors
getTermVectorsWith opts indexName (DocId docId) body =
  post
    ([unIndexName indexName, "_termvectors", docId] `withQueries` termVectorsOptionsParams opts)
    (encode body)

-- | 'getMultiTermVectors' returns information and statistics about
-- terms in the fields of multiple documents in a single request.
-- Maps to @POST \/_mtermvectors@ (no index in URL). Each
-- 'MultiTermVectorsDoc' must carry its own @_index@ since the URL
-- path does not supply one. For the single-index form, see
-- 'getMultiTermVectorsByIndex'.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-multi-termvectors.html>
-- and
-- <https://docs.opensearch.org/latest/api-reference/document-apis/multi-term-vectors/>.
getMultiTermVectors ::
  MultiTermVectors ->
  BHRequest StatusDependant MultiTermVectorsResponse
getMultiTermVectors = getMultiTermVectorsWith defaultTermVectorsOptions

-- | Like 'getMultiTermVectors' but accepts 'TermVectorsOptions'
-- carrying the URI-level parameters documented at
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-multi-termvectors.html docs-multi-termvectors>.
-- 'defaultTermVectorsOptions' makes the wire output byte-identical to
-- 'getMultiTermVectors'.
getMultiTermVectorsWith ::
  TermVectorsOptions ->
  MultiTermVectors ->
  BHRequest StatusDependant MultiTermVectorsResponse
getMultiTermVectorsWith opts body =
  post
    (["_mtermvectors"] `withQueries` termVectorsOptionsParams opts)
    (encode body)

-- | 'getMultiTermVectorsByIndex' is the single-index form of
-- 'getMultiTermVectors'. Maps to
-- @POST \/{index}\/_mtermvectors@. The 'IndexName' is supplied in
-- the URL, so each 'MultiTermVectorsDoc' built by 'mkMultiTermVectorsDoc'
-- (no @_index@) is sufficient.
getMultiTermVectorsByIndex ::
  IndexName ->
  [DocId] ->
  BHRequest StatusDependant MultiTermVectorsResponse
getMultiTermVectorsByIndex = getMultiTermVectorsByIndexWith defaultTermVectorsOptions

-- | Like 'getMultiTermVectorsByIndex' but accepts 'TermVectorsOptions'
-- carrying the URI-level parameters documented at
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-multi-termvectors.html docs-multi-termvectors>.
-- 'defaultTermVectorsOptions' makes the wire output byte-identical to
-- 'getMultiTermVectorsByIndex'.
getMultiTermVectorsByIndexWith ::
  TermVectorsOptions ->
  IndexName ->
  [DocId] ->
  BHRequest StatusDependant MultiTermVectorsResponse
getMultiTermVectorsByIndexWith opts indexName docIds =
  post
    ([unIndexName indexName, "_mtermvectors"] `withQueries` termVectorsOptionsParams opts)
    (encode body)
  where
    body = MultiTermVectors (map mkMultiTermVectorsDoc docIds)

documentExists :: IndexName -> DocId -> BHRequest StatusDependant Bool
documentExists = documentExistsWith defaultDocumentExistsOptions

-- | Like 'documentExists' but accepts 'DocumentExistsOptions' carrying
-- the URI-level parameters documented at
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-get.html#docs-get-api-query-params docs-get-api-query-params>
-- (@stored_fields@, @preference@, @realtime@, @refresh@, @routing@,
-- @version@, @version_type@). 'defaultDocumentExistsOptions' makes
-- this byte-for-byte equivalent to 'documentExists'.
documentExistsWith ::
  DocumentExistsOptions ->
  IndexName ->
  DocId ->
  BHRequest StatusDependant Bool
documentExistsWith opts indexName (DocId docId) =
  doesExist ([unIndexName indexName, "_doc", docId] `withQueries` documentExistsOptionsParams opts)

-- | 'documentSourceExists' checks whether the @_source@ field is
-- present for a document, hitting
-- @HEAD \/{index}\/_source\/{id}@. Returns 'True' on any 2xx
-- response and 'False' otherwise (including 404 when the document or
-- its source are absent). This is the source-only counterpart of
-- 'documentExists' (which HEADs the document itself) and the
-- existence-check counterpart of 'getDocumentSource' (which GETs the
-- source body). Uses the cross-version-safe @\/{index}\/_source\/{id}@
-- path (see 'getDocumentSource' for why the legacy @_doc\/{id}\/_source@
-- form is not used).
--
-- Like the other @*Exists@ helpers built on 'doesExist', this never
-- throws on absence: any non-2xx status decodes to 'False'.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-get.html#get-source-api>,
-- <https://docs.opensearch.org/latest/api-reference/document-apis/get-documents/>)
documentSourceExists :: IndexName -> DocId -> BHRequest StatusDependant Bool
documentSourceExists = documentSourceExistsWith defaultDocumentSourceExistsOptions

-- | Like 'documentSourceExists' but accepts
-- 'DocumentSourceExistsOptions' carrying the URI-level parameters
-- documented at
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-get.html#get-source-api docs-get-source-api>
-- (@preference@, @realtime@, @refresh@, @routing@, @version@,
-- @version_type@). Note that @stored_fields@ is not honoured by the
-- source endpoint and is therefore absent from this record; use
-- 'documentExistsWith' for the full parameter set.
-- 'defaultDocumentSourceExistsOptions' makes this byte-for-byte
-- equivalent to 'documentSourceExists'.
documentSourceExistsWith ::
  DocumentSourceExistsOptions ->
  IndexName ->
  DocId ->
  BHRequest StatusDependant Bool
documentSourceExistsWith opts indexName (DocId docId) =
  doesExist ([unIndexName indexName, "_source", docId] `withQueries` documentSourceExistsParams opts)

-- | Backward-compatible alias for @'dispatchSearchWith' 'defaultSearchOptions'@.
-- Kept around so pre-existing callers (and the scroll helpers below) don't
-- have to thread 'SearchOptions' if they don't want any.
dispatchSearch :: (FromJSON a) => Endpoint -> Search -> BHRequest StatusDependant (SearchResult a)
dispatchSearch endpoint search =
  dispatchSearchWith endpoint defaultSearchOptions search

-- | Like 'dispatchSearch' but accepts a 'SearchOptions' record carrying the
-- URI-level search parameters (@preference@, @routing@, @request_cache@,
-- @expand_wildcards@, etc.) documented at
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-search.html#search-search-api-query-params the ES docs>
-- and
-- <https://docs.opensearch.org/latest/api-reference/search/ the OpenSearch docs>.
--
-- 'defaultSearchOptions' emits no parameters, making this byte-for-byte
-- equivalent to 'dispatchSearch'.
dispatchSearchWith ::
  (FromJSON a) =>
  Endpoint ->
  SearchOptions ->
  Search ->
  BHRequest StatusDependant (SearchResult a)
dispatchSearchWith endpoint opts search =
  post url' (encode search)
  where
    url' = endpoint `withQueries` searchOptionsParams opts (searchType search)

-- | Render a 'SearchOptions' record plus the always-present 'SearchType' as
-- a list of @key=value@ query parameters suitable for 'withQueries'.
-- 'Nothing' fields are omitted; the @search_type@ parameter is only emitted
-- when it differs from the server default ('SearchTypeQueryThenFetch').
--
-- The order of the returned list is stable but /unspecified/ — callers
-- (including tests) should treat it as a set and not pattern-match on the
-- head. The underlying 'withQueries' is order-insensitive.
searchOptionsParams :: SearchOptions -> SearchType -> [(Text, Maybe Text)]
searchOptionsParams SearchOptions {..} st =
  stParam
    <> catMaybes
      [ boolParam "allow_no_indices" <$> soAllowNoIndices,
        ("expand_wildcards",) . Just . renderExpandWildcards <$> soExpandWildcards,
        boolParam "ignore_unavailable" <$> soIgnoreUnavailable,
        ("preference",) . Just <$> soPreference,
        ("routing",) . Just <$> soRouting,
        boolParam "request_cache" <$> soRequestCache,
        boolParam "typed_keys" <$> soTypedKeys,
        boolParam "seq_no_primary_term" <$> soSeqNoPrimaryTerm,
        intParam "max_concurrent_shard_requests" <$> soMaxConcurrentShardRequests,
        intParam "batched_reduce_size" <$> soBatchedReduceSize,
        boolParam "ccs_minimize_roundtrips" <$> soCcsMinimizeRoundtrips,
        boolParam "allow_partial_search_results" <$> soAllowPartialSearchResults,
        intParam "pre_filter_shard_size" <$> soPreFilterShardSize,
        ("cancel_after_time_interval",) . Just . renderDuration <$> soCancelAfterTimeInterval,
        intParam "max_concurrent_searches" <$> soMaxConcurrentSearches,
        ("_source",) . Just . boolQP <$> soSource,
        ("_source_includes",) . Just <$> soSourceIncludes,
        ("_source_excludes",) . Just <$> soSourceExcludes
      ]
  where
    -- 'SearchTypeQueryThenFetch' is the server default, so omit it to
    -- preserve the legacy wire shape of 'dispatchSearch'.
    stParam
      | st == SearchTypeDfsQueryThenFetch = [("search_type", Just "dfs_query_then_fetch")]
      | otherwise = []
    boolParam k b = (k, Just (boolQP b))
    intParam k n = (k, Just (showText n))
    renderExpandWildcards = T.intercalate "," . map expandWildcardsText . toList
    renderDuration (u, n) = showText n <> timeUnitsSuffix u

-- | 'searchAll', given a 'Search', will perform that search against all indexes
--  on an Elasticsearch server. Try to avoid doing this if it can be helped.
--
-- >>> let query = TermQuery (Term "user" "bitemyapp") Nothing
-- >>> let search = mkSearch (Just query) Nothing
-- >>> response <- runBH' $ searchAll search
searchAll :: (FromJSON a) => Search -> BHRequest StatusDependant (SearchResult a)
searchAll =
  searchAllWith defaultSearchOptions

-- | 'searchAllWith' is a variant of 'searchAll' that accepts a
-- 'SearchOptions' record for URI-level parameters.
searchAllWith ::
  (FromJSON a) =>
  SearchOptions ->
  Search ->
  BHRequest StatusDependant (SearchResult a)
searchAllWith opts =
  dispatchSearchWith ["_search"] opts

-- | 'searchByIndex', given a 'Search' and an 'IndexName', will perform that search
--  within an index on an Elasticsearch server.
--
-- >>> let query = TermQuery (Term "user" "bitemyapp") Nothing
-- >>> let search = mkSearch (Just query) Nothing
-- >>> response <- runBH' $ searchByIndex testIndex search
searchByIndex :: (FromJSON a) => IndexName -> Search -> BHRequest StatusDependant (SearchResult a)
searchByIndex indexName =
  searchByIndexWith indexName defaultSearchOptions

-- | 'searchByIndexWith' is a variant of 'searchByIndex' that accepts a
-- 'SearchOptions' record for URI-level parameters such as @preference@,
-- @routing@, and @request_cache@.
searchByIndexWith ::
  (FromJSON a) =>
  IndexName ->
  SearchOptions ->
  Search ->
  BHRequest StatusDependant (SearchResult a)
searchByIndexWith indexName opts =
  dispatchSearchWith [unIndexName indexName, "_search"] opts

-- | 'searchByIndices' is a variant of 'searchByIndex' that executes a
--  'Search' over many indices. This is much faster than using
--  'mapM' to 'searchByIndex' over a collection since it only
--  causes a single HTTP request to be emitted.
searchByIndices :: (FromJSON a) => NonEmpty IndexName -> Search -> BHRequest StatusDependant (SearchResult a)
searchByIndices ixs =
  searchByIndicesWith ixs defaultSearchOptions

-- | 'searchByIndicesWith' is a variant of 'searchByIndices' that accepts a
-- 'SearchOptions' record for URI-level parameters.
searchByIndicesWith ::
  (FromJSON a) =>
  NonEmpty IndexName ->
  SearchOptions ->
  Search ->
  BHRequest StatusDependant (SearchResult a)
searchByIndicesWith ixs opts =
  dispatchSearchWith [renderedIxs, "_search"] opts
  where
    renderedIxs = T.intercalate (T.singleton ',') (map unIndexName (toList ixs))

-- | 'explainDocument' computes a score explanation for a single
-- document under a given 'Query', mirroring the
-- @POST \/{index}\/_explain\/{id}@ endpoint. The 'Query' is wrapped
-- on the wire as @{"query": ...}@ (via 'ExplainQuery'), which is the
-- shape @_explain@ actually accepts. A full 'Search' cannot be sent:
-- the server rejects the @from@\/@size@\/@track_scores@ (etc.) fields
-- that 'Search' always serialises.
--
-- The response is parsed into an 'ExplainResponse'. Three outcomes
-- are distinguishable:
--
--   * 'explainResponseMatched' is @True@ — the document matched;
--     'explainResponseExplanation' is @Just@ an 'Explanation' tree.
--   * 'explainResponseMatched' is @False@ /and/
--     'explainResponseExplanation' is @Just (Explanation 0.0 ...)@ —
--     the document exists but the query did not match; the server
--     returns a synthetic explanation showing why (typically with
--     @value@ 0.0 and a description like @"no matching term"@).
--   * 'explainResponseMatched' is @False@ /and/
--     'explainResponseExplanation' is 'Nothing' — the document does
--     not exist. The server returns HTTP 404 with a body of the form
--     @{"_index":...,"_id":...,"matched":false}@, which decodes as a
--     minimal 'ExplainResponse'. ('StatusIndependant' is used so this
--     body is decoded directly rather than being routed through the
--     error-parser; a future revision could surface it as a
--     'Left'-'EsError' if callers prefer.)
--
-- Malformed requests (e.g. an unparseable 'Query') surface as
-- @'Left' 'EsError'@ carrying the server's HTTP status (the body
-- cannot be decoded as an 'ExplainResponse' so the wrapper falls
-- back to a synthetic parse-error message).
--
-- Equivalent to @'explainDocumentWith' 'defaultExplainOptions'@: no
-- URI parameters are emitted, preserving the legacy wire shape. Use
-- 'explainDocumentWith' to set @routing@, @preference@,
-- @stored_fields@, @_source@ filtering, or the query-parsing
-- parameters (@default_operator@, @analyzer@, @df@,
-- @analyze_wildcard@, @lenient@).
--
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-explain.html ES docs>,
-- <https://docs.opensearch.org/latest/api-reference/search-apis/explain/ OpenSearch docs>.
explainDocument ::
  IndexName ->
  DocId ->
  Query ->
  BHRequest StatusIndependant ExplainResponse
explainDocument = explainDocumentWith defaultExplainOptions

-- | Like 'explainDocument' but accepts an 'ExplainOptions' record
-- carrying the URI-level parameters documented for @_explain@
-- (@routing@, @preference@, @stored_fields@, @_source@,
-- @_source_includes@\/@_source_excludes@, @lenient@,
-- @default_operator@, @df@, @analyzer@, @analyze_wildcard@).
-- 'defaultExplainOptions' makes this byte-for-byte equivalent to
-- 'explainDocument'.
explainDocumentWith ::
  ExplainOptions ->
  IndexName ->
  DocId ->
  Query ->
  BHRequest StatusIndependant ExplainResponse
explainDocumentWith opts indexName (DocId docId) query =
  post ([unIndexName indexName, "_explain", docId] `withQueries` explainOptionsParams opts) (encode (ExplainQuery query))

dispatchMultiSearch ::
  (FromJSON a) =>
  Endpoint ->
  SearchOptions ->
  NonEmpty MultiSearchItem ->
  BHRequest StatusDependant (MultiSearchResponse a)
dispatchMultiSearch endpoint opts items =
  post endpoint' (encodeMultiSearchItems items)
  where
    -- The URI-level @search_type@ parameter is omitted by
    -- 'searchOptionsParams' under 'SearchTypeQueryThenFetch' (the server
    -- default). Per-item @search_type@ in 'multiSearchItemSearchType'
    -- overrides the URI level for that sub-request.
    endpoint' = endpoint `withQueries` searchOptionsParams opts SearchTypeQueryThenFetch

-- | 'multiSearch' performs a multi-search request against @_msearch@ with
-- 'defaultSearchOptions', preserving the legacy wire shape (no URI
-- parameters beyond what the server applies by default).
multiSearch ::
  (FromJSON a) =>
  NonEmpty MultiSearchItem ->
  BHRequest StatusDependant (MultiSearchResponse a)
multiSearch = multiSearchWith defaultSearchOptions

-- | 'multiSearchWith' is a variant of 'multiSearch' that accepts a
-- 'SearchOptions' record carrying URI-level parameters
-- (@max_concurrent_searches@, @typed_keys@, @pre_filter_shard_size@,
-- @ccs_minimize_roundtrips@, ...). Per-header fields live on each
-- 'MultiSearchItem'; the URI-level @search_type@ is intentionally
-- omitted by 'dispatchMultiSearch' (it conflicts with the per-item
-- semantics) so set 'multiSearchItemSearchType' to choose
-- @query_then_fetch@ vs @dfs_query_then_fetch@ per sub-request.
--
-- See <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-multi-search.html>
-- and <https://docs.opensearch.org/latest/api-reference/multi-search/>.
multiSearchWith ::
  (FromJSON a) =>
  SearchOptions ->
  NonEmpty MultiSearchItem ->
  BHRequest StatusDependant (MultiSearchResponse a)
multiSearchWith = dispatchMultiSearch ["_msearch"]

-- | 'multiSearchByIndex' runs an @_msearch@ scoped to a single index,
-- with each 'Search' wrapped in a 'MultiSearchItem' that has no per-item
-- header fields set. Uses 'defaultSearchOptions'.
multiSearchByIndex ::
  (FromJSON a) =>
  IndexName ->
  NonEmpty Search ->
  BHRequest StatusDependant (MultiSearchResponse a)
multiSearchByIndex = multiSearchByIndexWith defaultSearchOptions

-- | 'multiSearchByIndexWith' is a variant of 'multiSearchByIndex' that
-- accepts a 'SearchOptions' record. The per-item header fields are
-- cleared because the index is already pinned by the URL
-- (@\/{index}\/_msearch@) and the remaining header fields
-- (@routing@, @search_type@, @preference@, ...) have no natural
-- "all sub-searches get the same value" default at this layer; callers
-- who need per-item values should switch to 'multiSearchWith'.
multiSearchByIndexWith ::
  (FromJSON a) =>
  SearchOptions ->
  IndexName ->
  NonEmpty Search ->
  BHRequest StatusDependant (MultiSearchResponse a)
multiSearchByIndexWith opts indexName searches =
  dispatchMultiSearch [unIndexName indexName, "_msearch"] opts items
  where
    items = fmap emptyHeader searches
    emptyHeader s =
      MultiSearchItem
        { multiSearchItemIndex = Nothing,
          multiSearchItemRouting = Nothing,
          multiSearchItemSearchType = Nothing,
          multiSearchItemPreference = Nothing,
          multiSearchItemAllowPartialSearchResults = Nothing,
          multiSearchItemSearch = s
        }

dispatchMultiSearchTemplate ::
  (FromJSON a) =>
  Endpoint ->
  SearchOptions ->
  NonEmpty MultiSearchTemplateItem ->
  BHRequest StatusDependant (MultiSearchResponse a)
dispatchMultiSearchTemplate endpoint opts items =
  post endpoint' (encodeMultiSearchTemplateItems items)
  where
    -- Same URI plumbing as 'dispatchMultiSearch': the URI-level
    -- @search_type@ is omitted under 'SearchTypeQueryThenFetch' (the
    -- server default). Per-item @search_type@ in
    -- 'multiSearchTemplateItemSearchType' overrides the URI level for
    -- that sub-request.
    endpoint' = endpoint `withQueries` searchOptionsParams opts SearchTypeQueryThenFetch

-- | 'multiSearchTemplate' performs a multi-search template request
-- against @_msearch/template@ with 'defaultSearchOptions'. Each
-- 'MultiSearchTemplateItem' pairs a per-sub-request header (index,
-- routing, ...) with a 'SearchTemplate' body. The response shape is the
-- same as @_msearch@ — a 'MultiSearchResponse' of 'SearchResult's.
--
-- See <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-multi-search-template.html>
-- and <https://docs.opensearch.org/latest/api-reference/multi-search-template/>.
multiSearchTemplate ::
  (FromJSON a) =>
  NonEmpty MultiSearchTemplateItem ->
  BHRequest StatusDependant (MultiSearchResponse a)
multiSearchTemplate = multiSearchTemplateWith defaultSearchOptions

-- | 'multiSearchTemplateWith' is a variant of 'multiSearchTemplate' that
-- accepts a 'SearchOptions' record carrying URI-level parameters
-- (@max_concurrent_searches@, @typed_keys@, ...). Per-header fields live
-- on each 'MultiSearchTemplateItem'; the URI-level @search_type@ is
-- intentionally omitted by 'dispatchMultiSearchTemplate' (it conflicts
-- with the per-item semantics) so set
-- 'multiSearchTemplateItemSearchType' to choose @query_then_fetch@ vs
-- @dfs_query_then_fetch@ per sub-request.
--
-- See <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-multi-search-template.html>
-- and <https://docs.opensearch.org/latest/api-reference/multi-search-template/>.
multiSearchTemplateWith ::
  (FromJSON a) =>
  SearchOptions ->
  NonEmpty MultiSearchTemplateItem ->
  BHRequest StatusDependant (MultiSearchResponse a)
multiSearchTemplateWith = dispatchMultiSearchTemplate ["_msearch", "template"]

-- | 'multiSearchTemplateByIndex' runs an @_msearch/template@ scoped to a
-- single index, with each 'SearchTemplate' wrapped in a
-- 'MultiSearchTemplateItem' that has no per-item header fields set.
-- Uses 'defaultSearchOptions'.
multiSearchTemplateByIndex ::
  (FromJSON a) =>
  IndexName ->
  NonEmpty SearchTemplate ->
  BHRequest StatusDependant (MultiSearchResponse a)
multiSearchTemplateByIndex = multiSearchTemplateByIndexWith defaultSearchOptions

-- | 'multiSearchTemplateByIndexWith' is a variant of
-- 'multiSearchTemplateByIndex' that accepts a 'SearchOptions' record.
-- The per-item header fields are cleared because the index is already
-- pinned by the URL (@\/{index}\/_msearch\/template@); callers who need
-- per-item header values should switch to 'multiSearchTemplateWith'.
multiSearchTemplateByIndexWith ::
  (FromJSON a) =>
  SearchOptions ->
  IndexName ->
  NonEmpty SearchTemplate ->
  BHRequest StatusDependant (MultiSearchResponse a)
multiSearchTemplateByIndexWith opts indexName templates =
  dispatchMultiSearchTemplate [unIndexName indexName, "_msearch", "template"] opts items
  where
    items = fmap emptyHeader templates
    emptyHeader t =
      MultiSearchTemplateItem
        { multiSearchTemplateItemIndex = Nothing,
          multiSearchTemplateItemRouting = Nothing,
          multiSearchTemplateItemSearchType = Nothing,
          multiSearchTemplateItemPreference = Nothing,
          multiSearchTemplateItemAllowPartialSearchResults = Nothing,
          multiSearchTemplateItemSearchTemplate = t
        }

dispatchSearchTemplate ::
  (FromJSON a) =>
  Endpoint ->
  SearchTemplate ->
  BHRequest StatusDependant (SearchResult a)
dispatchSearchTemplate endpoint search =
  post endpoint $ encode search

-- | 'searchByIndexTemplate', given a 'SearchTemplate' and an 'IndexName', will perform that search
--  within an index on an Elasticsearch server.
--
-- >>> let query = SearchTemplateSource "{\"query\": { \"match\" : { \"{{my_field}}\" : \"{{my_value}}\" } }, \"size\" : \"{{my_size}}\"}"
-- >>> let search = mkSearchTemplate (Right query) Nothing
-- >>> response <- runBH' $ searchByIndexTemplate testIndex search
searchByIndexTemplate ::
  (FromJSON a) =>
  IndexName ->
  SearchTemplate ->
  BHRequest StatusDependant (SearchResult a)
searchByIndexTemplate indexName =
  dispatchSearchTemplate [unIndexName indexName, "_search", "template"]

-- | 'searchByIndicesTemplate' is a variant of 'searchByIndexTemplate' that executes a
--  'SearchTemplate' over many indices. This is much faster than using
--  'mapM' to 'searchByIndexTemplate' over a collection since it only
--  causes a single HTTP request to be emitted.
searchByIndicesTemplate ::
  (FromJSON a) =>
  NonEmpty IndexName ->
  SearchTemplate ->
  BHRequest StatusDependant (SearchResult a)
searchByIndicesTemplate ixs =
  dispatchSearchTemplate [renderedIxs, "_search", "template"]
  where
    renderedIxs = T.intercalate (T.singleton ',') (map unIndexName (toList ixs))

-- | 'storeSearchTemplate', saves a 'SearchTemplateSource' to be used later.
storeSearchTemplate :: SearchTemplateId -> SearchTemplateSource -> BHRequest StatusDependant Acknowledged
storeSearchTemplate (SearchTemplateId tid) ts =
  post ["_scripts", tid] (encode body)
  where
    body = Object $ X.fromList ["script" .= Object ("lang" .= String "mustache" <> "source" .= ts)]

-- | 'getSearchTemplate', get info of an stored 'SearchTemplateSource'.
getSearchTemplate :: SearchTemplateId -> BHRequest StatusIndependant GetTemplateScript
getSearchTemplate (SearchTemplateId tid) =
  get ["_scripts", tid]

-- | 'storeSearchTemplate',
deleteSearchTemplate :: SearchTemplateId -> BHRequest StatusIndependant Acknowledged
deleteSearchTemplate (SearchTemplateId tid) =
  delete ["_scripts", tid]

-- | 'renderTemplate' renders a 'SearchTemplate' as a search request
-- body /without/ executing the search. The endpoint is
-- @GET /_render/template@ for an inline template (a 'SearchTemplate'
-- whose 'searchTemplate' is @'Right' source@) or
-- @GET /_render/template\/{id}@ for a stored template (whose
-- 'searchTemplate' is @'Left' ('SearchTemplateId' id)@); the path
-- segment is therefore derived from 'searchTemplate'.
--
-- Unlike almost every other GET in the ES API, @_render/template@
-- sends its input in a request body (the encoded 'SearchTemplate':
-- @source@ \/ @id@, @params@, @explain@, @profile@), so the request
-- is built with 'getWithBody' rather than 'get'. The body is accepted
-- verbatim by both path variants; when the @id@ path variant is used,
-- a redundant @\"id\"@ key in the body is harmless — the server takes
-- the id from the path and ignores the body copy.
--
-- The rendered output is returned as a raw aeson 'Value'. The server
-- wraps it in a @{\"template_output\": ...}@ envelope; callers wanting
-- the rendered body itself extract the @\"template_output\"@ key.
-- Returning the whole body as 'Value' mirrors the @explainSQL@ /
-- @predict@ precedent (the only other free-form-'Value' endpoints in
-- the surface) and avoids baking in an envelope wrapper that future
-- server-side shape drift could invalidate. The request is
-- 'StatusDependant' and wrapped in 'ParsedEsResponse', so a 4xx (a
-- parse error in the template source, a missing stored id, ...) is
-- surfaced as @'Left' 'EsError'@ rather than thrown.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-render-template.html>)
renderTemplate ::
  SearchTemplate ->
  BHRequest StatusDependant (ParsedEsResponse Value)
renderTemplate search =
  withBHResponseParsedEsResponse $
    getWithBody @StatusDependant endpoint (encode search)
  where
    endpoint =
      case searchTemplate search of
        Left (SearchTemplateId tid) -> ["_render", "template", tid]
        Right _ -> ["_render", "template"]

-- | For a given search, request a scroll for efficient streaming of
-- search results. Note that the search is put into 'SearchTypeScan'
-- mode and thus results will not be sorted. Combine this with
-- 'advanceScroll' to efficiently stream through the full result set
getInitialScroll ::
  (FromJSON a) =>
  IndexName ->
  Search ->
  BHRequest StatusDependant (ParsedEsResponse (SearchResult a))
getInitialScroll indexName search' =
  withBHResponseParsedEsResponse $ dispatchSearch endpoint search
  where
    endpoint = [unIndexName indexName, "_search"] `withQueries` [("scroll", Just "1m")]
    sorting = Just [DefaultSortSpec $ mkSort (FieldName "_doc") Descending]
    search = search' {sortBody = sorting}

-- | For a given search, request a scroll for efficient streaming of
-- search results. Combine this with 'advanceScroll' to efficiently
-- stream through the full result set. Note that this search respects
-- sorting and may be less efficient than 'getInitialScroll'.
getInitialSortedScroll ::
  (FromJSON a) =>
  IndexName ->
  Search ->
  BHRequest StatusDependant (SearchResult a)
getInitialSortedScroll indexName search = do
  dispatchSearch endpoint search
  where
    endpoint = [unIndexName indexName, "_search"] `withQueries` [("scroll", Just "1m")]

-- | Use the given scroll to fetch the next page of documents. If there are no
-- further pages, 'SearchResult.searchHits.hits' will be '[]'.
--
-- Equivalent to @'advanceScrollWith' 'defaultAdvanceScrollOptions'@. Use
-- the @With@ variant to send @?rest_total_hits_as_int=true@ (useful when
-- you want the server to render @hits.total@ as a bare integer rather
-- than the @{value, relation}@ object).
advanceScroll ::
  (FromJSON a) =>
  ScrollId ->
  -- | How long should the snapshot of data be kept around? This timeout is updated every time 'advanceScroll' is used, so don't feel the need to set it to the entire duration of your search processing. Note that durations < 1s will be rounded up. Also note that 'NominalDiffTime' is an instance of Num so literals like 60 will be interpreted as seconds. 60s is a reasonable default.
  NominalDiffTime ->
  BHRequest StatusDependant (SearchResult a)
advanceScroll =
  advanceScrollWith defaultAdvanceScrollOptions

-- | Like 'advanceScroll' but accepts an 'AdvanceScrollOptions' record
-- carrying URI-level parameters for @POST /_search/scroll@. Currently
-- the only exposed parameter is @rest_total_hits_as_int@; see
-- 'AdvanceScrollOptions' for details.
--
-- @'advanceScrollWith' 'defaultAdvanceScrollOptions'@ is byte-for-byte
-- identical to 'advanceScroll'.
--
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/scroll-search.html>)
advanceScrollWith ::
  (FromJSON a) =>
  AdvanceScrollOptions ->
  ScrollId ->
  -- | How long should the snapshot of data be kept around? This timeout is updated every time 'advanceScroll' is used, so don't feel the need to set it to the entire duration of your search processing. Note that durations < 1s will be rounded up. Also note that 'NominalDiffTime' is an instance of Num so literals like 60 will be interpreted as seconds. 60s is a reasonable default.
  NominalDiffTime ->
  BHRequest StatusDependant (SearchResult a)
advanceScrollWith opts (ScrollId sid) scroll =
  post endpoint (encode scrollObject)
  where
    endpoint =
      ["_search", "scroll"] `withQueries` advanceScrollOptionsParams opts
    scrollTime = showText secs <> "s"
    secs :: Integer
    secs = round scroll

    scrollObject =
      object
        [ "scroll" .= scrollTime,
          "scroll_id" .= sid
        ]

-- | Release a scroll context immediately rather than waiting for its
-- @keep_alive@ to expire. Both Elasticsearch and OpenSearch accept a single
-- @scroll_id@ string or a JSON array of them; here we always send a single-ID
-- array. Use this to avoid leaking contexts up to the
-- @search.max_open_scroll_context@ limit (default 500).
--
-- Doc: https://www.elastic.co/guide/en/elasticsearch/reference/7.17/clear-scroll-api.html
clearScroll ::
  ScrollId ->
  BHRequest StatusDependant ClearScrollResponse
clearScroll (ScrollId sid) =
  deleteWithBody ["_search", "scroll"] (encode scrollObject)
  where
    scrollObject =
      object ["scroll_id" .= ([sid] :: [Text])]

-- | 'mkSearch' is a helper function for defaulting additional fields of a 'Search'
--  to Nothing in case you only care about your 'Query' and 'Filter'. Use record update
--  syntax if you want to add things like aggregations or highlights while still using
--  this helper function.
--
-- >>> let query = TermQuery (Term "user" "bitemyapp") Nothing
-- >>> mkSearch (Just query) Nothing
-- Search {queryBody = Just (TermQuery (Term {termField = "user", termValue = "bitemyapp"}) Nothing), filterBody = Nothing, searchAfterKey = Nothing, sortBody = Nothing, aggBody = Nothing, highlight = Nothing, trackSortScores = False, from = From 0, size = Size 10, searchType = SearchTypeQueryThenFetch, fields = Nothing, source = Nothing}
mkSearch :: Maybe Query -> Maybe Filter -> Search
mkSearch query filter =
  Search
    { queryBody = query,
      filterBody = filter,
      sortBody = Nothing,
      aggBody = Nothing,
      highlight = Nothing,
      trackSortScores = False,
      from = From 0,
      size = Size 10,
      searchType = SearchTypeQueryThenFetch,
      searchAfterKey = Nothing,
      fields = Nothing,
      scriptFields = Nothing,
      docvalueFields = Nothing,
      source = Nothing,
      suggestBody = Nothing,
      pointInTime = Nothing,
      knnBody = Nothing,
      osKnnBody = Nothing,
      trackTotalHits = Nothing,
      timeout = Nothing,
      minScore = Nothing,
      explain = Nothing,
      searchVersion = Nothing,
      seqNoPrimaryTerm = Nothing,
      terminateAfter = Nothing,
      stats = Nothing,
      searchPipeline = Nothing,
      storedFields = Nothing,
      runtimeMappings = Nothing,
      rescore = Nothing,
      collapse = Nothing,
      retriever = Nothing
    }

-- | 'mkAggregateSearch' is a helper function that defaults everything in a 'Search' except for
--  the 'Query' and the 'Aggregation'.
--
-- >>> let terms = TermsAgg $ (mkTermsAggregation "user") { termCollectMode = Just BreadthFirst }
-- >>> terms
-- TermsAgg (TermsAggregation {term = Left "user", termInclude = Nothing, termExclude = Nothing, termOrder = Nothing, termMinDocCount = Nothing, termSize = Nothing, termShardSize = Nothing, termCollectMode = Just BreadthFirst, termExecutionHint = Nothing, termAggs = Nothing})
-- >>> let myAggregation = mkAggregateSearch Nothing $ mkAggregations "users" terms
mkAggregateSearch :: Maybe Query -> Aggregations -> Search
mkAggregateSearch query mkSearchAggs =
  Search
    { queryBody = query,
      filterBody = Nothing,
      sortBody = Nothing,
      aggBody = Just mkSearchAggs,
      highlight = Nothing,
      trackSortScores = False,
      from = From 0,
      size = Size 0,
      searchType = SearchTypeQueryThenFetch,
      searchAfterKey = Nothing,
      fields = Nothing,
      scriptFields = Nothing,
      docvalueFields = Nothing,
      source = Nothing,
      suggestBody = Nothing,
      pointInTime = Nothing,
      knnBody = Nothing,
      osKnnBody = Nothing,
      trackTotalHits = Nothing,
      timeout = Nothing,
      minScore = Nothing,
      explain = Nothing,
      searchVersion = Nothing,
      seqNoPrimaryTerm = Nothing,
      terminateAfter = Nothing,
      stats = Nothing,
      searchPipeline = Nothing,
      storedFields = Nothing,
      runtimeMappings = Nothing,
      rescore = Nothing,
      collapse = Nothing,
      retriever = Nothing
    }

-- | 'mkHighlightSearch' is a helper function that defaults everything in a 'Search' except for
--  the 'Query' and the 'Aggregation'.
--
-- >>> let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
-- >>> let testHighlight = Highlights Nothing [FieldHighlight (FieldName "message") Nothing]
-- >>> let search = mkHighlightSearch (Just query) testHighlight
mkHighlightSearch :: Maybe Query -> Highlights -> Search
mkHighlightSearch query searchHighlights =
  Search
    { queryBody = query,
      filterBody = Nothing,
      sortBody = Nothing,
      aggBody = Nothing,
      highlight = Just searchHighlights,
      trackSortScores = False,
      from = From 0,
      size = Size 10,
      searchType = SearchTypeDfsQueryThenFetch,
      searchAfterKey = Nothing,
      fields = Nothing,
      scriptFields = Nothing,
      docvalueFields = Nothing,
      source = Nothing,
      suggestBody = Nothing,
      pointInTime = Nothing,
      knnBody = Nothing,
      osKnnBody = Nothing,
      trackTotalHits = Nothing,
      timeout = Nothing,
      minScore = Nothing,
      explain = Nothing,
      searchVersion = Nothing,
      seqNoPrimaryTerm = Nothing,
      terminateAfter = Nothing,
      stats = Nothing,
      searchPipeline = Nothing,
      storedFields = Nothing,
      runtimeMappings = Nothing,
      rescore = Nothing,
      collapse = Nothing,
      retriever = Nothing
    }

-- | 'mkSearchTemplate' is a helper function for defaulting additional fields of a 'SearchTemplate'
--  to Nothing. Use record update syntax if you want to add things.
mkSearchTemplate :: Either SearchTemplateId SearchTemplateSource -> TemplateQueryKeyValuePairs -> SearchTemplate
mkSearchTemplate id_ params = SearchTemplate id_ params Nothing Nothing

-- | 'pageSearch' is a helper function that takes a search and assigns the from
--   and size fields for the search. The from parameter defines the offset
--   from the first result you want to fetch. The size parameter allows you to
--   configure the maximum amount of hits to be returned.
--
-- >>> let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
-- >>> let search = mkSearch (Just query) Nothing
-- >>> search
-- Search {queryBody = Just (QueryMatchQuery (MatchQuery {matchQueryField = FieldName "_all", matchQueryQueryString = QueryString "haskell", matchQueryOperator = Nothing, matchQueryZeroTerms = Nothing, matchQueryCutoffFrequency = Nothing, matchQueryAnalyzer = Nothing, matchQueryMaxExpansions = Nothing, matchQueryLenient = Nothing, matchQueryBoost = Nothing, matchQueryMinimumShouldMatch = Nothing, matchQueryFuzziness = Nothing})), filterBody = Nothing, sortBody = Nothing, aggBody = Nothing, highlight = Nothing, trackSortScores = False, from = From 0, size = Size 10, searchType = SearchTypeQueryThenFetch, fields = Nothing, source = Nothing}
-- >>> pageSearch (From 10) (Size 100) search
-- Search {queryBody = Just (QueryMatchQuery (MatchQuery {matchQueryField = FieldName "_all", matchQueryQueryString = QueryString "haskell", matchQueryOperator = Nothing, matchQueryZeroTerms = Nothing, matchQueryCutoffFrequency = Nothing, matchQueryAnalyzer = Nothing, matchQueryMaxExpansions = Nothing, matchQueryLenient = Nothing, matchQueryBoost = Nothing, matchQueryMinimumShouldMatch = Nothing, matchQueryFuzziness = Nothing})), filterBody = Nothing, sortBody = Nothing, aggBody = Nothing, highlight = Nothing, trackSortScores = False, from = From 10, size = Size 100, searchType = SearchTypeQueryThenFetch, fields = Nothing, source = Nothing}
pageSearch ::
  -- | The result offset
  From ->
  -- | The number of results to return
  Size ->
  -- | The current seach
  Search ->
  -- | The paged search
  Search
pageSearch resultOffset pageSize search = search {from = resultOffset, size = pageSize}

boolQP :: Bool -> Text
boolQP True = "true"
boolQP False = "false"

-- | 'countByIndex' counts the documents matching 'CountQuery' in a
-- single index. Wraps @POST \/{index}\/_count@
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-count.html>,
-- <https://docs.opensearch.org/latest/api-reference/search-apis/count/>).
--
-- This is the simple, parameterless form; it is byte-for-byte
-- identical to a call made with 'defaultCountOptions'. Pass URI
-- parameters (@allow_no_indices@, @expand_wildcards@,
-- @ignore_unavailable@, @min_score@, @preference@, @routing@)
-- via 'countByIndexWith', and count across the whole cluster (or a
-- list of indices) via 'countAll' \/ 'countByIndexWith'.
countByIndex :: IndexName -> CountQuery -> BHRequest StatusDependant CountResponse
countByIndex indexName q =
  countByIndexWith (Just [indexName]) defaultCountOptions q

-- | 'countByIndexWith' is the fully-parameterised form of
-- 'countByIndex'. Every URI parameter accepted by @/_count@ is exposed
-- via 'CountOptions'. The index argument selects the request path:
--
--   * 'Nothing' or @'Just' []@ — @POST /_count@, counts across every
--     index on the cluster (this is also what 'countAll' does).
--   * @'Just' [i]@ — @POST \/{i}\/_count@, equivalent to 'countByIndex'.
--   * @'Just' (i : is)@ — @POST \/{i,is...}\/_count@, with the index
--     names joined by commas; the server expands wildcard patterns and
--     honours 'coExpandWildcards'.
countByIndexWith ::
  Maybe [IndexName] ->
  CountOptions ->
  CountQuery ->
  BHRequest StatusDependant CountResponse
countByIndexWith mIndices opts q =
  post @StatusDependant endpoint (encode q)
  where
    endpoint = path `withQueries` countOptionsParams opts
    path =
      case mIndices of
        Nothing -> ["_count"]
        Just [] -> ["_count"]
        Just (first : rest) ->
          [T.intercalate "," (map unIndexName (first : rest)), "_count"]

-- | 'countAll' counts the documents matching 'CountQuery' across every
-- index on the cluster. Wraps @POST /_count@ (without an index path
-- segment). It is 'countByIndexWith' with the index argument set to
-- 'Nothing' and 'defaultCountOptions'; pass URI parameters by calling
-- 'countByIndexWith' directly.
countAll :: CountQuery -> BHRequest StatusDependant CountResponse
countAll q = countByIndexWith Nothing defaultCountOptions q

-- $validateOverview
--
-- The @_validate/query@ endpoints check whether a 'Query' parses
-- against the resolved index mapping without executing it. They are
-- shared verbatim by Elasticsearch (7.x\/8.x\/9.x) and OpenSearch
-- (1.x\/2.x\/3.x).
--
--   * <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-validate.html ES docs>
--   * <https://docs.opensearch.org/latest/api-reference/search-apis/validate/ OpenSearch docs>

-- | 'validateQuery' validates a 'Query' against a single index. Wraps
-- @POST \/{index}/_validate/query@
-- (<https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-validate.html>,
-- <https://docs.opensearch.org/latest/api-reference/search-apis/validate/>).
--
-- This is the simple, parameterless form; it is byte-for-byte
-- identical to a call made with 'defaultValidateOptions'. Pass URI
-- parameters (@explain@, @all@, @expand_wildcards@, etc.) via
-- 'validateQueryWith', and validate across the whole cluster (or a
-- list of indices) via 'validateAll' \/ 'validateQueryWith'.
--
-- On the minimal response (no @explain@, no @all@) the server
-- returns @{"valid":bool}@; 'validateQueryResponseShards' and
-- 'validateQueryResponseExplanations' will both be 'Nothing'. Use
-- 'validateQueryWith' with @voExplain = Just True@ to obtain the
-- per-index 'ValidateExplanation' breakdown.
validateQuery ::
  IndexName ->
  ValidateQuery ->
  BHRequest StatusDependant ValidateQueryResponse
validateQuery indexName q =
  validateQueryWith (Just [indexName]) defaultValidateOptions q

-- | 'validateQueryWith' is the fully-parameterised form of
-- 'validateQuery'. Every URI parameter accepted by
-- @/_validate/query@ is exposed via 'ValidateOptions'. The index
-- argument selects the request path:
--
--   * 'Nothing' or @'Just' []@ — @POST /_validate/query@, validates
--     against every index on the cluster (this is also what
--     'validateAll' does).
--   * @'Just' [i]@ — @POST \/{i}/_validate/query@, equivalent to
--     'validateQuery'.
--   * @'Just' (i : is)@ — @POST \/{i,is...}/_validate/query@, with
--     the index names joined by commas; the server expands wildcard
--     patterns and honours 'voExpandWildcards'.
validateQueryWith ::
  Maybe [IndexName] ->
  ValidateOptions ->
  ValidateQuery ->
  BHRequest StatusDependant ValidateQueryResponse
validateQueryWith mIndices opts q =
  post @StatusDependant endpoint (encode q)
  where
    endpoint = path `withQueries` validateOptionsParams opts
    path =
      case mIndices of
        Nothing -> ["_validate", "query"]
        Just [] -> ["_validate", "query"]
        Just (first : rest) ->
          [ T.intercalate "," (map unIndexName (first : rest)),
            "_validate",
            "query"
          ]

-- | 'validateAll' validates a 'Query' across every index on the
-- cluster. Wraps @POST /_validate/query@ (without an index path
-- segment). It is 'validateQueryWith' with the index argument set to
-- 'Nothing' and 'defaultValidateOptions'; pass URI parameters by
-- calling 'validateQueryWith' directly.
validateAll :: ValidateQuery -> BHRequest StatusDependant ValidateQueryResponse
validateAll q = validateQueryWith Nothing defaultValidateOptions q

-- $rankEvalOverview
--
-- The @_rank_eval@ endpoints evaluate the quality of ranked search
-- results against a set of known relevance ratings. They are shared
-- verbatim by Elasticsearch (7.x\/8.x\/9.x) and OpenSearch
-- (1.x\/2.x\/3.x).
--
--   * <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-rank-eval.html ES docs>
--   * <https://docs.opensearch.org/latest/api-reference/search-apis/rank-eval/ OpenSearch docs>

-- | 'evaluateRank' runs a rank-evaluation request against every index
-- on the cluster. Wraps @POST /_rank_eval@ with
-- 'defaultRankEvalOptions'. Pass a 'RankEvalMetric' via the
-- 'RankEvalRequest' to override the server default (mean reciprocal
-- rank with @k=20@); pass URI parameters via 'evaluateRankWith', and
-- scope to a single index (or a comma-joined list) via
-- 'evaluateRankByIndex'.
evaluateRank ::
  RankEvalRequest ->
  BHRequest StatusDependant RankEvalResponse
evaluateRank = evaluateRankWith Nothing defaultRankEvalOptions

-- | 'evaluateRankByIndex' is the single-index form of 'evaluateRank'.
-- Maps to @POST \/{index}\/_rank_eval@. Use 'evaluateRankWith' to
-- target multiple comma-joined indices or to forward URI parameters
-- (@search_type@, @allow_no_indices@, @expand_wildcards@,
-- @ignore_unavailable@).
evaluateRankByIndex ::
  IndexName ->
  RankEvalRequest ->
  BHRequest StatusDependant RankEvalResponse
evaluateRankByIndex indexName = evaluateRankWith (Just [indexName]) defaultRankEvalOptions

-- | 'evaluateRankWith' is the fully-parameterised form of
-- 'evaluateRank'. The index argument selects the request path:
--
--   * 'Nothing' or @'Just' []@ — @POST /_rank_eval@, evaluates across
--     every index on the cluster (this is also what 'evaluateRank'
--     does).
--   * @'Just' [i]@ — @POST \/{i}\/_rank_eval@, equivalent to
--     'evaluateRankByIndex'.
--   * @'Just' (i : is)@ — @POST \/{i,is...}\/_rank_eval@, with the
--     index names joined by commas; the server expands wildcard
--     patterns and honours 'reoExpandWildcards'.
evaluateRankWith ::
  Maybe [IndexName] ->
  RankEvalOptions ->
  RankEvalRequest ->
  BHRequest StatusDependant RankEvalResponse
evaluateRankWith mIndices opts body =
  post @StatusDependant endpoint (encode body)
  where
    endpoint = path `withQueries` rankEvalOptionsParams opts
    path =
      case mIndices of
        Nothing -> ["_rank_eval"]
        Just [] -> ["_rank_eval"]
        Just (first : rest) ->
          [T.intercalate "," (map unIndexName (first : rest)), "_rank_eval"]

-- | 'getFieldCaps' returns the capabilities of fields (searchable,
-- aggregatable, type, contributing indices) across the given indices
-- (or every index on the cluster when 'Nothing' or @'Just' []@ is
-- supplied). The field patterns restrict which fields are returned;
-- pass @[]@ to ask for every field. Maps to @POST /_field_caps@ or
-- @POST \/{index}\/_field_caps@.
--
-- Use 'getFieldCapsWith' to send an @index_filter@ query, runtime
-- mappings, or additional URI parameters (@allow_no_indices@,
-- @expand_wildcards@, @ignore_unavailable@, @include_unmapped@).
getFieldCaps ::
  Maybe [IndexName] ->
  [FieldPattern] ->
  BHRequest StatusDependant FieldCapsResponse
getFieldCaps mIndices fields =
  getFieldCapsWith mIndices opts defaultFieldCapsRequest
  where
    opts =
      defaultFieldCapsOptions
        { fcoFields = case fields of
            [] -> Nothing
            xs -> Just xs
        }

-- | 'getFieldCapsWith' is the fully-parameterised form of
-- 'getFieldCaps'. Every URI parameter accepted by @/_field_caps@ is
-- exposed via 'FieldCapsOptions' (including the @fields@ array, which
-- is sent as a comma-joined URI query parameter for cross-version
-- compatibility — Elasticsearch ≤ 7.x does not accept @fields@ in the
-- body). The 'FieldCapsRequest' body controls the @index_filter@
-- query and the @runtime_mappings@ object.
getFieldCapsWith ::
  Maybe [IndexName] ->
  FieldCapsOptions ->
  FieldCapsRequest ->
  BHRequest StatusDependant FieldCapsResponse
getFieldCapsWith mIndices opts req =
  post @StatusDependant endpoint (encode req)
  where
    endpoint = path `withQueries` fieldCapsOptionsParams opts
    path =
      case mIndices of
        Nothing -> ["_field_caps"]
        Just [] -> ["_field_caps"]
        Just (first : rest) ->
          [T.intercalate "," (map unIndexName (first : rest)), "_field_caps"]

-- | 'getSearchShards' returns the shards and nodes that a search
-- request would be executed against, without running the search
-- itself. Pass 'Nothing' or @'Just' []@ to target every index the
-- caller can see; pass a non-empty list to restrict the routing
-- computation to a comma-joined subset. Issues
-- @POST \/{indices}\/_search_shards@ (the server also accepts @GET@
-- but takes no body, so the bodyless 'postNoBody' variant is used).
--
-- Equivalent to @'getSearchShardsWith' 'defaultSearchShardsOptions'@.
-- Use the @With@ variant to pass @allow_no_indices@,
-- @expand_wildcards@, @ignore_unavailable@, @local@, @master_timeout@,
-- @preference@ or @routing@.
getSearchShards ::
  Maybe [IndexName] ->
  BHRequest StatusDependant SearchShardsResponse
getSearchShards mIndices =
  getSearchShardsWith mIndices defaultSearchShardsOptions

-- | 'getSearchShardsWith' is the fully-parameterised form of
-- 'getSearchShards'. Every URI parameter accepted by
-- @/{indices}/_search_shards@ is exposed via 'SearchShardsOptions'.
-- 'defaultSearchShardsOptions' makes this byte-for-byte identical to
-- 'getSearchShards'.
getSearchShardsWith ::
  Maybe [IndexName] ->
  SearchShardsOptions ->
  BHRequest StatusDependant SearchShardsResponse
getSearchShardsWith mIndices opts =
  postNoBody @StatusDependant (path `withQueries` searchShardsOptionsParams opts)
  where
    path =
      case mIndices of
        Nothing -> ["_search_shards"]
        Just [] -> ["_search_shards"]
        Just (first : rest) ->
          [T.intercalate "," (map unIndexName (first : rest)), "_search_shards"]

-- $vectorTile
--
-- /Vector tile search/ renders the results of a geospatial search as a
-- binary Mapbox Vector Tile (MVT, encoded as a Google Protobuf). The
-- endpoint was added in Elasticsearch 7.15.0 and is also available in
-- OpenSearch; the wire format is identical, so the request builder
-- lives in the Common layer.

-- | 'searchVectorTile' issues a
-- @GET \/{index}\/_mvt\/{field}\/{zoom}\/{x}\/{y}@ request and returns
-- the response body verbatim as a lazy 'L.ByteString'. The body is a
-- Mapbox Vector Tile encoded as a Google Protobuf (PBF); callers are
-- responsible for decoding it with a library such as @vector-tile@.
--
-- The 'Search' argument supplies the @query@, @aggs@, @sort@, @fields@,
-- @runtime_mappings@, @size@ and @track_total_hits@ body fields. MVT
-- does not accept the full 'Search' surface — server-side validation
-- rejects unknown fields such as @from@, @collapse@, @highlight@ and
-- @suggest@. The Haddock on 'searchVectorTileWith' lists the body
-- fields the endpoint accepts.
--
-- Equivalent to @'searchVectorTileWith' 'defaultVectorTileOptions'@:
-- the URI parameter list is empty, so every server-side body default
-- (@extent = 4096@, @grid_precision = 8@, @size = 10000@,
-- @exact_bounds = false@, @buffer = 5@) applies. Use the @With@
-- variant to override any of these via the corresponding URI parameter
-- (which takes precedence over the body value per the ES docs).
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-vector-tile-api.html>
-- and
-- <https://docs.opensearch.org/latest/search-plugins/search-vector-tile-api/>.
searchVectorTile ::
  IndexName ->
  FieldName ->
  TileZoom ->
  TileX ->
  TileY ->
  Search ->
  BHRequest StatusDependant L.ByteString
searchVectorTile indexName field zoom x y =
  searchVectorTileWith indexName field zoom x y defaultVectorTileOptions

-- | 'searchVectorTileWith' is the fully-parameterised form of
-- 'searchVectorTile'. The MVT-specific URI parameters
-- ('VectorTileOptions') override the JSON body values when both are
-- set. The body fields the endpoint accepts are:
--
--   [@query@]         standard query DSL (filter the @hits@ layer).
--   [@aggs@]          sub-aggregations on each @geotile_grid@ /
--                     @geohex_grid@ cell (names cannot start with
--                     @_mvt_@).
--   [@sort@]          hit ordering (@_score@, @_doc@,
--                     @_geo_distance@, @_script@).
--   [@fields@]        fields to return in the @hits@ layer.
--   [@runtime_mappings@] runtime field definitions.
--   [@size@]          max @hits@ features (0–10000; @0@ omits the
--                     layer).
--   [@track_total_hits@] accurate hit counting.
--
-- The MVT-specific extras (@buffer@, @extent@, @grid_agg@,
-- @grid_precision@, @grid_type@, @exact_bounds@, @with_labels@) have
-- no home on the 'Search' record; configure them via 'VectorTileOptions'
-- so they ride as URI parameters.
searchVectorTileWith ::
  IndexName ->
  FieldName ->
  TileZoom ->
  TileX ->
  TileY ->
  VectorTileOptions ->
  Search ->
  BHRequest StatusDependant L.ByteString
searchVectorTileWith indexName (FieldName field) (TileZoom zoom) (TileX x) (TileY y) opts search =
  getByteStringWithBody
    (endpoint `withQueries` vectorTileOptionsParams opts)
    (encode search)
  where
    endpoint =
      [ unIndexName indexName,
        "_mvt",
        field,
        showText zoom,
        showText x,
        showText y
      ]

-- | 'analyzeText' runs the analysis pipeline on the supplied text
-- without indexing it. Maps to @POST /_analyze@ when the index is
-- 'Nothing', or @POST \/{index}\/_analyze@ when an 'IndexName' is
-- supplied (the analyzer configuration of that index is then
-- available for reference by name in 'analyzeRequestAnalyzer',
-- 'analyzeRequestField', etc.).
--
-- The 'AnalyzeRequest' only references analyzers, tokenizers, filters
-- and normalizers by name; inline analyzer definitions and the
-- @explain@ response variant are not supported by this function.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/indices-analyze.html>
-- and
-- <https://docs.opensearch.org/latest/api-reference/index-apis/analyze/>.
analyzeText ::
  Maybe IndexName ->
  AnalyzeRequest ->
  BHRequest StatusDependant AnalyzeResponse
analyzeText mIndex req =
  post @StatusDependant endpoint (encode req)
  where
    endpoint =
      case mIndex of
        Nothing -> ["_analyze"]
        Just indexName -> [unIndexName indexName, "_analyze"]

reindex ::
  ReindexRequest ->
  BHRequest StatusDependant ReindexResponse
reindex = reindexWith defaultReindexOptions

-- | Like 'reindex' but accepts 'ReindexOptions' carrying the URI-level
-- parameters documented at
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-reindex.html#docs-reindex-api-query-params docs-reindex-api-query-params>.
-- 'defaultReindexOptions' makes this byte-for-byte equivalent to
-- 'reindex'.
reindexWith ::
  ReindexOptions ->
  ReindexRequest ->
  BHRequest StatusDependant ReindexResponse
reindexWith opts req =
  post endpoint (encode req)
  where
    endpoint = ["_reindex"] `withQueries` reindexOptionsParams opts

reindexAsync ::
  ReindexRequest ->
  BHRequest StatusDependant TaskNodeId
reindexAsync = reindexAsyncWith defaultReindexOptions

-- | Like 'reindexAsync' but accepts 'ReindexOptions' carrying the
-- URI-level parameters documented at
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-reindex.html#docs-reindex-api-query-params docs-reindex-api-query-params>.
-- 'defaultReindexOptions' makes this byte-for-byte equivalent to
-- 'reindexAsync'. @wait_for_completion=false@ is always appended
-- (overriding any value the caller might supply via a future option),
-- because this entry point is defined to return immediately with the
-- 'TaskNodeId' of the background task — synchronous callers should use
-- 'reindexWith' instead.
reindexAsyncWith ::
  ReindexOptions ->
  ReindexRequest ->
  BHRequest StatusDependant TaskNodeId
reindexAsyncWith opts req =
  post endpoint (encode req)
  where
    endpoint =
      ["_reindex"]
        `withQueries` ( reindexOptionsParams opts
                          <> [("wait_for_completion", Just "false")]
                      )

-- | 'rethrottleReindex' changes the maximum documents-per-second rate
-- of an in-progress asynchronous reindex. Maps to
-- @POST /_reindex/{task_id}/_rethrottle?requests_per_second=<n>@. The
-- 'TaskNodeId' is the composite @nodeId:localId@ identifier returned by
-- 'reindexAsync' or observed via 'listTasks' \/ 'getTask'.
--
-- The response uses the same nodes-grouped shape as 'cancelTask'
-- (a 'TaskListResponse' listing the task(s) whose rate was changed),
-- /not/ an 'Acknowledged' — the ES wire format is a task list even
-- though the operation is conceptually an acknowledgement. An empty
-- node list typically means the task had already finished by the time
-- the request reached the server.
--
-- Rethrottling that speeds the reindex up takes effect immediately;
-- rethrottling that slows it down takes effect after the current
-- batch completes (to prevent scroll timeouts).
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docs-reindex.html#docs-reindex-rethrottle-api docs-reindex-rethrottle-api>.
rethrottleReindex ::
  TaskNodeId ->
  RethrottleRate ->
  BHRequest StatusDependant TaskListResponse
rethrottleReindex (TaskNodeId task) rate =
  post endpoint emptyBody
  where
    endpoint =
      ["_reindex", task, "_rethrottle"]
        `withQueries` [("requests_per_second", Just (renderRethrottleRate rate))]

-- | Legacy 'getTask'; equivalent to @'getTaskWith' 'defaultTaskGetOptions'@.
getTask ::
  (FromJSON a) =>
  TaskNodeId ->
  BHRequest StatusDependant (TaskResponse a)
getTask = getTaskWith defaultTaskGetOptions

-- | 'getTask' with explicit URI parameters. Maps to
-- @GET /_tasks/{task_id}@. Pass 'defaultTaskGetOptions' to reproduce
-- the legacy parameterless behaviour; populate the fields to send
-- @wait_for_completion@ (block until the task finishes) or @timeout@
-- (how long to wait).
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/tasks.html#get-task>
-- and
-- <https://docs.opensearch.org/latest/api-reference/tasks/#get-a-task>.
getTaskWith ::
  (FromJSON a) =>
  TaskGetOptions ->
  TaskNodeId ->
  BHRequest StatusDependant (TaskResponse a)
getTaskWith opts (TaskNodeId task) =
  get $ ["_tasks", task] `withQueries` taskGetOptionsParams opts

-- | Render the 'TaskGetOptions' URI parameters (@wait_for_completion@,
-- @timeout@) as a list of @(key, value)@ pairs suitable for
-- 'withQueries'. 'Nothing' fields are omitted, so
-- 'defaultTaskGetOptions' produces an empty list (and therefore no
-- query string).
taskGetOptionsParams :: TaskGetOptions -> [(Text, Maybe Text)]
taskGetOptionsParams opts =
  catMaybes
    [ ("wait_for_completion",) . Just . renderBool <$> taskGetOptionsWaitForCompletion opts,
      ("timeout",) . Just . renderDuration <$> taskGetOptionsTimeout opts
    ]
  where
    renderDuration (u, n) = showText n <> timeUnitsSuffix u
    renderBool True = "true"
    renderBool False = "false"

-- | 'cancelTask' cancels a currently running task by id. Maps to
-- @POST /_tasks/{task_id}/_cancel@. The response uses the same
-- nodes-grouped shape as 'listTasks' — it lists the tasks that were
-- actually cancelled — so this builder returns a 'TaskListResponse',
-- which may be empty if the task had already finished by the time the
-- request reached the server. The 'TaskNodeId' is the composite
-- @nodeId:localId@ identifier returned by
-- 'Database.Bloodhound.Common.Client.reindexAsync' or observed via
-- 'listTasks' or 'getTask'.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/tasks-cancel.html>.
cancelTask ::
  TaskNodeId ->
  BHRequest StatusDependant TaskListResponse
cancelTask (TaskNodeId task) =
  post ["_tasks", task, "_cancel"] emptyBody

-- | 'listTasks' lists currently running tasks on the cluster. Maps to
-- @GET /_tasks@. Pass 'Nothing' to call the bare endpoint, or
-- @'Just' 'defaultTaskListOptions'@ to reproduce it with explicit
-- parameters; pass a populated 'TaskListOptions' to filter by node,
-- action, parent task, etc. or to request @detailed@ task bodies.
--
-- The structured 'TaskListResponse' preserves the node grouping of the
-- server's default @group_by=nodes@; flatten it with 'taskListFlat'.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/tasks.html#list-tasks>.
listTasks ::
  Maybe TaskListOptions ->
  BHRequest StatusDependant TaskListResponse
listTasks opts =
  get $ ["_tasks"] `withQueries` taskListOptionsParams (fromMaybe defaultTaskListOptions opts)

-- | Render the 'TaskListOptions' URI parameters (@nodes@, @actions@,
-- @detailed@, @parent_task_id@, @wait_for_completion@, @timeout@,
-- @group_by@) as a list of @(key, value)@ pairs suitable for
-- 'withQueries'. 'Nothing' fields are omitted, so
-- 'defaultTaskListOptions' produces an empty list (and therefore no
-- query string).
taskListOptionsParams :: TaskListOptions -> [(Text, Maybe Text)]
taskListOptionsParams opts =
  catMaybes
    [ ("nodes",) . Just . T.intercalate "," <$> taskListOptionsNodes opts,
      ("actions",) . Just . T.intercalate "," <$> taskListOptionsActions opts,
      ("detailed",) . Just . renderBool <$> taskListOptionsDetailed opts,
      ("parent_task_id",) . Just <$> taskListOptionsParentTaskId opts,
      ("wait_for_completion",) . Just . renderBool <$> taskListOptionsWaitForCompletion opts,
      ("timeout",) . Just . renderDuration <$> taskListOptionsTimeout opts,
      ("group_by",) . Just . renderTaskListGroupBy <$> taskListOptionsGroupBy opts
    ]
  where
    renderDuration (u, n) = showText n <> timeUnitsSuffix u
    renderBool True = "true"
    renderBool False = "false"

-- $asyncSearch
--
-- /Async Search/ submits a long-running search to the background and
-- returns an identifier that can be polled for partial or final results.
-- Available since Elasticsearch 7.7, so the request lives in the Common
-- layer and is re-exported by every client module. (OpenSearch does not
-- implement this API; calls against OS will fail at runtime with a 404.)

-- | 'getAsyncSearch' retrieves the current state and (partial or final)
-- results of an async search. Maps to @GET /_async_search/{id}@.
--
-- While the search is running, 'asyncSearchIsRunning' is 'True' and the
-- search-result fields (@asyncSearchHits@, @asyncSearchTook@, ...) are
-- 'Nothing'; once complete, they are populated.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/async-search.html#get-async-search>.
getAsyncSearch ::
  (FromJSON a) =>
  AsyncSearchId ->
  BHRequest StatusDependant (AsyncSearchResult a)
getAsyncSearch (AsyncSearchId searchId) =
  get ["_async_search", searchId]

-- | 'getAsyncSearchStatus' reports only the running\/partial state and
-- scheduling times for an async search — /not/ the partial or final
-- results. Maps to @GET /_async_search/status/{id}@ and returns the
-- narrower 'AsyncSearchStatus' shape (no @hits@, @aggregations@, ...).
--
-- Useful when polling for completion without paying the cost of
-- materialising the result set on each call.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/async-search.html#get-async-search-status>.
getAsyncSearchStatus ::
  AsyncSearchId ->
  BHRequest StatusDependant AsyncSearchStatus
getAsyncSearchStatus (AsyncSearchId searchId) =
  get ["_async_search", "status", searchId]

-- $ilm
--
-- /Index Lifecycle Management/ is the Elasticsearch-native rollover\/shrink\/
-- delete automation (unrelated to OpenSearch's ISM plugin). Available on
-- Elasticsearch 7.x and later, so the request lives in the Common layer and
-- is re-exported by every ES client module.

-- | 'getILMPolicy' lists ILM policies.
--
-- * @'Nothing'@  → @GET /_ilm/policy@ returns every policy on the cluster.
-- * @'Just' pid@ → @GET /_ilm/policy/{pid}@ returns just that one (as a
--   singleton list).
--
-- The response is a JSON object keyed by policy id, so the body decoder
-- walks the keys and folds each one into the corresponding 'ILMPolicyInfo'.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/ilm-get-lifecycle.html>.
getILMPolicy ::
  Maybe ILMPolicyId ->
  BHRequest StatusDependant [ILMPolicyInfo]
getILMPolicy mPolicyId =
  unILMPolicies <$> get @StatusDependant endpoint
  where
    endpoint = case mPolicyId of
      Nothing -> ["_ilm", "policy"]
      Just (ILMPolicyId pid) -> ["_ilm", "policy", pid]

-- | 'putILMPolicy' creates or updates a single ILM policy by id. Maps to
-- @PUT /_ilm/policy/{id}@. The 'ILMPolicy' body wraps the policy proper
-- (phases, actions, ...) under the @policy@ key that ES expects; see its
-- 'ToJSON' instance.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/ilm-put-lifecycle.html>.
putILMPolicy ::
  ILMPolicyId ->
  ILMPolicy ->
  BHRequest StatusDependant Acknowledged
putILMPolicy (ILMPolicyId pid) policy =
  put ["_ilm", "policy", pid] (encode policy)

-- | 'deleteILMPolicy' deletes a single ILM policy by id. Maps to
-- @DELETE /_ilm/policy/{id}@ and returns 'Acknowledged' on success.
-- Deleting a policy that does not exist fails with an HTTP error
-- (hence 'StatusDependant'), surfaced as an 'EsError'. The wire
-- format is identical across ES 7.x, 8.x and 9.x, so the request
-- builder lives in the Common layer and is re-exported by every ES
-- client module.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/ilm-delete-lifecycle.html>.
deleteILMPolicy ::
  ILMPolicyId ->
  BHRequest StatusDependant Acknowledged
deleteILMPolicy (ILMPolicyId pid) =
  delete ["_ilm", "policy", pid]

-- | 'explainILM' retrieves the ILM state for every index matched by the
-- given 'IndexName' (which may be a wildcard or a comma-separated list).
-- Maps to @GET /_ilm/explain/{index}@. The response is keyed by the
-- concrete matched index names; this function returns the bare
-- ['ILMExplanation'] (each of which carries its own 'ilmExplanationIndex').
--
-- Equivalent to @'explainILMWith' 'defaultILMExplainOptions'@. See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/ilm-explain-lifecycle.html>.
explainILM ::
  IndexName ->
  BHRequest StatusDependant ILMExplanations
explainILM = explainILMWith defaultILMExplainOptions

-- | Like 'explainILM' but accepts the full 'ILMExplainOptions' record,
-- exposing the @only_errors@, @only_managed@ and @master_timeout@ URI
-- parameters. 'defaultILMExplainOptions' makes this byte-for-byte
-- equivalent to 'explainILM'.
explainILMWith ::
  ILMExplainOptions ->
  IndexName ->
  BHRequest StatusDependant ILMExplanations
explainILMWith opts indexName =
  get endpoint
  where
    endpoint =
      ["_ilm", "explain", unIndexName indexName]
        `withQueries` ilmExplainOptionsParams opts

-- | Render the 'ILMExplainOptions' URI parameters (@only_errors@,
-- @only_managed@, @master_timeout@) as a list of @(key, value)@ pairs
-- suitable for 'withQueries'. 'Nothing' fields are omitted, so
-- 'defaultILMExplainOptions' produces an empty list (and therefore no
-- query string).
ilmExplainOptionsParams :: ILMExplainOptions -> [(Text, Maybe Text)]
ilmExplainOptionsParams opts =
  catMaybes
    [ ("only_errors",) . Just . renderBool <$> ilmeoOnlyErrors opts,
      ("only_managed",) . Just . renderBool <$> ilmeoOnlyManaged opts,
      ("master_timeout",) . Just . renderDuration <$> ilmeoMasterTimeout opts
    ]
  where
    renderBool True = "true"
    renderBool False = "false"
    renderDuration (u, n) = showText n <> timeUnitsSuffix u

-- --------------------------------------------------------------------------
-- ILM control (start/stop/status/move/retry/remove)
------------------------------------------------------------------------------

-- | 'startILM' starts the ILM plugin. Maps to @POST /_ilm/start@ and
-- returns 'Acknowledged' on success. ILM runs by default, so this is
-- only needed to resume after a 'stopILM'.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/ilm-start.html>.
startILM :: BHRequest StatusDependant Acknowledged
startILM = post ["_ilm", "start"] emptyBody

-- | 'stopILM' stops the ILM plugin. Maps to @POST /_ilm/stop@ and
-- returns 'Acknowledged' on success. While stopped, ILM will not manage
-- any indices; use 'startILM' to resume.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/ilm-stop.html>.
stopILM :: BHRequest StatusDependant Acknowledged
stopILM = post ["_ilm", "stop"] emptyBody

-- | 'getILMStatus' reports the cluster-wide ILM operation mode.
-- Maps to @GET /_ilm/status@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/ilm-get-status.html>.
getILMStatus :: BHRequest StatusDependant ILMStatus
getILMStatus = get ["_ilm", "status"]

-- | 'moveILMStep' forces the given index from one lifecycle step into
-- another. Maps to @POST /_ilm/move/{index}@. Both the @current_step@
-- and @next_step@ of the 'MoveStepRequest' body must match steps the
-- index could legally reach; ES rejects moves across unrelated branches.
-- Returns 'Acknowledged' on success.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/ilm-move-to-step.html>.
moveILMStep ::
  IndexName ->
  MoveStepRequest ->
  BHRequest StatusDependant Acknowledged
moveILMStep indexName body =
  post ["_ilm", "move", unIndexName indexName] (encode body)

-- | 'retryILMStep' retries the current ILM step for an index that has
-- entered an error state. Maps to @POST /_ilm/retry/{index}@ and
-- returns 'Acknowledged' on success.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/ilm-retry-policy.html>.
retryILMStep ::
  IndexName ->
  BHRequest StatusDependant Acknowledged
retryILMStep indexName =
  post ["_ilm", "retry", unIndexName indexName] emptyBody

-- | 'removeILM' detaches the given index from ILM management. Maps to
-- @POST /_ilm/remove/{index}@ and returns 'Acknowledged' on success.
-- The index keeps its current state but ILM stops driving it.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/ilm-remove-policy.html>.
removeILM ::
  IndexName ->
  BHRequest StatusDependant Acknowledged
removeILM indexName =
  post ["_ilm", "remove", unIndexName indexName] emptyBody

-- | 'migrateDataTiers' migrates the cluster's allocation routing from the
-- legacy @node.attr.*@ style to the modern data-tier style
-- (@data_hot@ \/ @data_warm@ \/ ...). Maps to
-- @POST /_ilm/migrate_to_data_tiers@ and returns a
-- 'MigrateDataTiersResponse' describing what was migrated.
--
-- Equivalent to
-- @'migrateDataTiersWith' 'defaultMigrateDataTiersOptions' 'defaultMigrateDataTiersRequest'@:
-- an empty body (auto-detected node attribute and legacy template) with
-- @dry_run@ unset. Use the @With@ variant to preview the migration
-- ('migrateDataTiersOptionsDryRun' = 'Just' 'True') or to override the
-- auto-detected values.
--
-- Available on Elasticsearch 7.14 and later. The wire format is
-- identical across ES 7.14+, 8.x and 9.x, so the request builder lives
-- in the Common layer and is re-exported by every ES client module.
--
-- See
-- <https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-ilm-migrate-to-data-tiers>.
migrateDataTiers ::
  BHRequest StatusDependant MigrateDataTiersResponse
migrateDataTiers =
  migrateDataTiersWith
    defaultMigrateDataTiersOptions
    defaultMigrateDataTiersRequest

-- | 'migrateDataTiersWith' is the fully-parameterised form of
-- 'migrateDataTiers'. The 'MigrateDataTiersOptions' argument exposes the
-- @dry_run@ URI parameter; the 'MigrateDataTiersRequest' argument exposes
-- the optional @legacy_template_to_delete@ and @node_attribute@ body
-- fields. 'defaultMigrateDataTiersOptions' and
-- 'defaultMigrateDataTiersRequest' together reproduce 'migrateDataTiers'.
migrateDataTiersWith ::
  MigrateDataTiersOptions ->
  MigrateDataTiersRequest ->
  BHRequest StatusDependant MigrateDataTiersResponse
migrateDataTiersWith opts body =
  post @StatusDependant endpoint (encode body)
  where
    endpoint =
      ["_ilm", "migrate_to_data_tiers"]
        `withQueries` migrateDataTiersOptionsParams opts

-- | Render the 'MigrateDataTiersOptions' URI parameters (@dry_run@) as a
-- list of @(key, value)@ pairs suitable for 'withQueries'. 'Nothing'
-- fields are omitted, so 'defaultMigrateDataTiersOptions' produces an
-- empty list (and therefore no query string).
migrateDataTiersOptionsParams :: MigrateDataTiersOptions -> [(Text, Maybe Text)]
migrateDataTiersOptionsParams opts =
  catMaybes
    [ ("dry_run",) . Just . renderBool <$> migrateDataTiersOptionsDryRun opts
    ]
  where
    renderBool True = "true"
    renderBool False = "false"

-- --------------------------------------------------------------------------
-- Snapshot Lifecycle Management (SLM)
------------------------------------------------------------------------------

-- $slm
--
-- /Snapshot Lifecycle Management/ is the Elasticsearch-native automation
-- for taking periodic snapshots of cluster indices and applying retention
-- rules (unrelated to ILM, which rolls indices over\/shrinks\/deletes).
-- Available on Elasticsearch 7.4 and later, so every request lives in the
-- Common layer and is re-exported by every ES client module. The full
-- surface is covered:
--
-- * @PUT /_slm/policy/{id}@ — create/update a policy ('putSLMPolicy').
-- * @GET /_slm/policy[/{id}]@ — list one or all policies ('getSLMPolicy').
-- * @DELETE /_slm/policy/{id}@ — delete a policy ('deleteSLMPolicy').
-- * @POST /_slm/execute/{id}@ — run a policy out of schedule
--   ('executeSLMPolicy').
-- * @GET /_slm/status@ — read the operation mode and snapshot counters
--   ('getSLMStatus').
-- * @POST /_slm/stop@ — stop SLM scheduling ('stopSLM').

-- | 'putSLMPolicy' creates or updates a single snapshot lifecycle policy by
-- id. Maps to @PUT /_slm/policy/{id}@ and returns 'Acknowledged' on
-- success. Unlike 'putILMPolicy', the 'SLMPolicy' body is sent as-is at
-- the top level of the request — SLM does @not@ wrap the policy under a
-- @policy@ key, so callers must build the JSON object directly (see
-- "Database.Bloodhound.Internal.Versions.Common.Types.SLM" for an example).
--
-- The wire format is identical across ES 7.x, 8.x and 9.x, so the request
-- builder lives in the Common layer and is re-exported by every ES client
-- module.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/put-slm-lifecycle-policy.html>.
putSLMPolicy ::
  SLMPolicyId ->
  SLMPolicy ->
  BHRequest StatusDependant Acknowledged
putSLMPolicy (SLMPolicyId pid) policy =
  put ["_slm", "policy", pid] (encode policy)

-- | 'getSLMPolicy' lists SLM policies.
--
-- * @'Nothing'@  → @GET /_slm/policy@ returns every policy on the cluster.
-- * @'Just' pid@ → @GET /_slm/policy/{pid}@ returns just that one (as a
--   singleton list).
--
-- The response is a JSON object keyed by policy id, so the body decoder
-- walks the keys and folds each one into the corresponding
-- 'SLMPolicyInfo' (overriding the placeholder id set by the
-- 'SLMPolicyInfo' decoder).
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/get-slm-lifecycle-policy.html>.
getSLMPolicy ::
  Maybe SLMPolicyId ->
  BHRequest StatusDependant [SLMPolicyInfo]
getSLMPolicy mPolicyId =
  unSLMPolicies <$> get @StatusDependant endpoint
  where
    endpoint = case mPolicyId of
      Nothing -> ["_slm", "policy"]
      Just (SLMPolicyId pid) -> ["_slm", "policy", pid]

-- | 'deleteSLMPolicy' deletes a single SLM policy by id. Maps to
-- @DELETE /_slm/policy/{id}@ and returns 'Acknowledged' on success.
-- Deleting a policy that does not exist fails with an HTTP error (hence
-- 'StatusDependant'), surfaced as an 'EsError'.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/delete-slm-lifecycle-policy.html>.
deleteSLMPolicy ::
  SLMPolicyId ->
  BHRequest StatusDependant Acknowledged
deleteSLMPolicy (SLMPolicyId pid) =
  delete ["_slm", "policy", pid]

-- | 'executeSLMPolicy' triggers a single SLM policy immediately, outside
-- of its schedule. Maps to @POST /_slm/execute/{id}@ and returns the
-- 'SnapshotName' of the newly-created snapshot on success.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/execute-slm-lifecycle-policy.html>.
executeSLMPolicy ::
  SLMPolicyId ->
  BHRequest StatusDependant SnapshotName
executeSLMPolicy (SLMPolicyId pid) =
  slmExecutionSnapshot
    <$> post @StatusDependant ["_slm", "execute", pid] emptyBody

-- | 'getSLMStatus' reports the cluster-wide SLM operation mode
-- ('SLMOperationMode') alongside the cumulative snapshot creation and
-- deletion counters. Maps to @GET /_slm/status@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/slm-get-status.html>.
getSLMStatus ::
  BHRequest StatusDependant SLMStatus
getSLMStatus = get ["_slm", "status"]

-- | 'stopSLM' stops all SLM scheduling. Maps to @POST /_slm/stop@ and
-- returns 'Acknowledged' on success. In-flight snapshot executions are
-- allowed to finish; no new snapshots will be scheduled until SLM is
-- restarted (via the corresponding 'startSLM', or the next cluster
-- restart).
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/stop-slm.html>.
stopSLM :: BHRequest StatusDependant Acknowledged
stopSLM = post ["_slm", "stop"] emptyBody

-- | 'startSLM' (re)starts SLM scheduling after a 'stopSLM' (or a managed
-- halt). Maps to @POST /_slm/start@ and returns 'Acknowledged' on
-- success. This is the inverse of 'stopSLM'.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/slm-api-start.html>.
--
-- @since 0.26.0.0
startSLM :: BHRequest StatusDependant Acknowledged
startSLM = post ["_slm", "start"] emptyBody

-- --------------------------------------------------------------------------
-- Watcher
------------------------------------------------------------------------------

-- $watcher
--
-- /Watcher/ is the Elasticsearch X-Pack alerting feature (Platinum or
-- Enterprise licence). A watch is a schedule + input + condition + actions
-- definition: Watcher evaluates the condition on the schedule and runs the
-- actions (email, webhook, index, logging, ...) when it holds. Available on
-- Elasticsearch 2.1+; the REST surface is identical across ES 7.x, 8.x and
-- 9.x (deprecated in 9.x but still present), so every request lives in the
-- Common layer and is re-exported by every ES client module. OpenSearch does
-- not implement Watcher, so calls against an OpenSearch cluster will fail at
-- runtime. The full surface is covered:
--
-- * @PUT /_watcher/watch/{id}@ — create/update a watch ('putWatch').
-- * @GET /_watcher/watch/{id}@ — retrieve a watch ('getWatch').
-- * @DELETE /_watcher/watch/{id}@ — delete a watch ('deleteWatch').
-- * @POST /_watcher/watch/{id}/_execute@ — dry-run a watch
--   ('executeWatch' / 'executeWatchWith').
-- * @PUT /_watcher/watch/{id}/_ack@ — acknowledge a watch's actions
--   ('ackWatch').
-- * @PUT /_watcher/watch/{id}/_activate@ — (re)activate a watch
--   ('activateWatch').
-- * @PUT /_watcher/watch/{id}/_deactivate@ — pause a watch
--   ('deactivateWatch').
-- * @GET /_watcher/_stats[/{metric}]@ — cluster-wide Watcher statistics
--   ('watcherStats' / 'watcherStatsWith').
-- * @GET /_watcher/settings@ — read Watcher cluster settings
--   ('getWatcherSettings').
-- * @PUT /_watcher/settings@ — update Watcher cluster settings
--   ('updateWatcherSettings').
-- * @POST /_watcher/_start@ — start the Watcher service ('startWatcher').
-- * @POST /_watcher/_stop@ — stop the Watcher service ('stopWatcher').

-- | 'putWatch' creates or updates a single watch by id. Maps to
-- @PUT /_watcher/watch/{id}@ and returns 'Acknowledged' on success. The
-- 'WatchBody' is carried as an opaque 'Data.Aeson.Value' — see
-- "Database.Bloodhound.Internal.Versions.Common.Types.Watcher" for the
-- rationale (the watch DSL is large and version-evolving).
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/watcher-api-put-watch.html>.
putWatch ::
  WatchId ->
  WatchBody ->
  BHRequest StatusDependant Acknowledged
putWatch (WatchId wid) body =
  put ["_watcher", "watch", wid] (encode body)

-- | 'getWatch' retrieves a single watch by id. Maps to
-- @GET /_watcher/watch/{id}@ and returns the full 'Watch' (typed envelope
-- + opaque watch DSL). A missing watch surfaces as an 'EsError' (hence
-- 'StatusDependant').
--
-- Watcher has no list-all-watches endpoint; every GET is by id.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/watcher-api-get-watch.html>.
getWatch :: WatchId -> BHRequest StatusDependant Watch
getWatch (WatchId wid) = get ["_watcher", "watch", wid]

-- | 'deleteWatch' deletes a single watch by id. Maps to
-- @DELETE /_watcher/watch/{id}@ and returns 'Acknowledged' on success.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/watcher-api-delete-watch.html>.
deleteWatch :: WatchId -> BHRequest StatusDependant Acknowledged
deleteWatch (WatchId wid) = delete ["_watcher", "watch", wid]

-- | 'executeWatch' dry-runs the stored watch with the given id. Maps to
-- @POST /_watcher/watch/{id}/_execute@ with no body and returns the
-- execution record on success. Equivalent to
-- @'executeWatchWith' 'defaultExecuteWatchOptions' wid 'Nothing'@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/watcher-api-execute-watch.html>.
executeWatch ::
  WatchId ->
  BHRequest StatusDependant ExecuteWatchResponse
executeWatch wid = executeWatchWith defaultExecuteWatchOptions wid Nothing

-- | 'executeWatchWith' is the fully-parameterised form of 'executeWatch'.
-- Every URI parameter accepted by @POST /_watcher/watch/{id}/_execute@ is
-- exposed via 'ExecuteWatchOptions'; supplying a 'Just' 'ExecuteWatchRequest'
-- body runs an inline watch instead of the stored one.
executeWatchWith ::
  ExecuteWatchOptions ->
  WatchId ->
  Maybe ExecuteWatchRequest ->
  BHRequest StatusDependant ExecuteWatchResponse
executeWatchWith opts (WatchId wid) mBody =
  post endpoint body
  where
    endpoint =
      ["_watcher", "watch", wid, "_execute"]
        `withQueries` executeWatchOptionsParams opts
    body = maybe emptyBody (encode . unExecuteWatchRequest) mBody

-- | 'ackWatch' acknowledges the actions of a watch by id. Maps to
-- @PUT /_watcher/watch/{id}/_ack@ and returns the watch's updated
-- acknowledge state.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/watcher-api-ack-watch.html>.
ackWatch :: WatchId -> BHRequest StatusDependant AckWatchResponse
ackWatch (WatchId wid) = put ["_watcher", "watch", wid, "_ack"] emptyBody

-- | 'activateWatch' (re)activates a watch by id. Maps to
-- @PUT /_watcher/watch/{id}/_activate@ and returns the watch's updated
-- state.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/watcher-api-activate-watch.html>.
activateWatch :: WatchId -> BHRequest StatusDependant WatchStateResponse
activateWatch (WatchId wid) =
  put ["_watcher", "watch", wid, "_activate"] emptyBody

-- | 'deactivateWatch' pauses a watch by id. Maps to
-- @PUT /_watcher/watch/{id}/_deactivate@ and returns the watch's updated
-- state.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/watcher-api-deactivate-watch.html>.
deactivateWatch ::
  WatchId ->
  BHRequest StatusDependant WatchStateResponse
deactivateWatch (WatchId wid) =
  put ["_watcher", "watch", wid, "_deactivate"] emptyBody

-- | 'watcherStats' reports cluster-wide Watcher statistics. Maps to
-- @GET /_watcher/_stats@ (every metric). Equivalent to
-- @'watcherStatsWith' 'Nothing'@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/watcher-api-stats.html>.
watcherStats :: BHRequest StatusDependant WatcherStatsResponse
watcherStats = watcherStatsWith Nothing

-- | 'watcherStatsWith' is the fully-parameterised form of 'watcherStats'.
-- Supplying 'Just' a 'WatcherStatsMetric' narrows the response to that
-- metric; 'Nothing' (or 'Just' 'WatcherStatsMetricAll') requests every
-- metric.
watcherStatsWith ::
  Maybe WatcherStatsMetric ->
  BHRequest StatusDependant WatcherStatsResponse
watcherStatsWith Nothing = get ["_watcher", "_stats"]
watcherStatsWith (Just metric) =
  get ["_watcher", "_stats", watcherStatsMetricPath metric]

-- | 'getWatcherSettings' reads the cluster-wide Watcher settings. Maps to
-- @GET /_watcher/settings@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/watcher-api-update-settings.html>.
getWatcherSettings ::
  BHRequest StatusDependant WatcherSettings
getWatcherSettings = get ["_watcher", "settings"]

-- | 'updateWatcherSettings' updates the cluster-wide Watcher settings.
-- Maps to @PUT /_watcher/settings@ and returns 'Acknowledged' on success.
-- The 'WatcherSettings' body shape is the standard persistent\/transient
-- cluster-settings envelope.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/watcher-api-update-settings.html>.
updateWatcherSettings ::
  WatcherSettings ->
  BHRequest StatusDependant Acknowledged
updateWatcherSettings body =
  put ["_watcher", "settings"] (encode body)

-- | 'startWatcher' starts the Watcher service cluster-wide. Maps to
-- @POST /_watcher/_start@ and returns 'Acknowledged' on success. This is
-- the inverse of 'stopWatcher'.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/watcher-api-start.html>.
startWatcher :: BHRequest StatusDependant Acknowledged
startWatcher = post ["_watcher", "_start"] emptyBody

-- | 'stopWatcher' stops the Watcher service cluster-wide. Maps to
-- @POST /_watcher/_stop@ and returns 'Acknowledged' on success. In-flight
-- watch executions are allowed to finish; no new watches will fire until
-- the service is restarted (via 'startWatcher').
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/watcher-api-stop.html>.
stopWatcher :: BHRequest StatusDependant Acknowledged
stopWatcher = post ["_watcher", "_stop"] emptyBody

------------------------------------------------------------------------------
-- Text structure API
--
-- /Text structure/ is the Elasticsearch text-classification surface
-- (@/_text_structure/*@) that inspects a sample of plain-text, NDJSON or
-- stored field values and infers its format, charset, delimiter, column
-- names, suggested ES mappings and ingest pipeline, and per-field stats.
-- The REST surface is identical across ES 7.17, 8.x and 9.x (it shipped
-- under the X-Pack flag on older 7.x and is part of the basic / free
-- license tier on 7.17+), so every request lives in the Common layer
-- and is re-exported by every ES client module. OpenSearch does not
-- implement these endpoints.
--

-- * @POST /_text_structure/find_structure@ — 'findStructure' /

--   'findStructureWith' (raw text body).
--

-- * @POST /_text_structure/find_message_structure@ —

--   'findMessageStructure' / 'findMessageStructureWith' (JSON
--   @{"messages":[...]}@ body).
--

-- * @GET /_text_structure/find_field_structure@ —

--   'findFieldStructure' / 'findFieldStructureWith' (no body; @index@
--   and @field@ select the stored values to sample).
--

-- * @POST /_text_structure/test_grok_pattern@ — 'testGrokPattern' /

--   'testGrokPatternWith' (JSON body; compiles a grok pattern and
--   matches it against the supplied strings).

-- | 'findStructure' inspects a sample of plain-text or NDJSON data and
-- returns the structure the server inferred (format, charset,
-- delimiter, suggested ES mappings, etc.). Maps to
-- @POST /_text_structure/find_structure@.
--
-- The /request body/ is the raw sample (a lazy 'L.ByteString'), not
-- JSON — it is forwarded verbatim, so callers should hand the function
-- an NDJSON or plain-text stream directly. The /response/ is JSON and
-- is decoded into 'FindStructureResponse'.
--
-- Equivalent to @'findStructureWith' 'defaultFindStructureOptions'@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/8.17/find-structure.html>.
findStructure ::
  L.ByteString ->
  BHRequest StatusDependant FindStructureResponse
findStructure = findStructureWith defaultFindStructureOptions

-- | 'findStructureWith' is the fully-parameterised form of
-- 'findStructure'. Every URI parameter accepted by
-- @POST /_text_structure/find_structure@ is exposed via
-- 'FindStructureOptions'.
findStructureWith ::
  FindStructureOptions ->
  L.ByteString ->
  BHRequest StatusDependant FindStructureResponse
findStructureWith opts sample =
  post @StatusDependant endpoint sample
  where
    endpoint =
      ["_text_structure", "find_structure"]
        `withQueries` findStructureOptionsParams opts

-- | 'findMessageStructure' classifies an explicit list of log messages
-- and returns the inferred structure. Maps to
-- @POST /_text_structure/find_message_structure@. Unlike
-- 'findStructure', the request body is JSON (@{"messages":[...]}@,
-- modelled by 'FindMessageStructureRequest'); the response reuses
-- 'FindStructureResponse'.
--
-- Equivalent to @'findMessageStructureWith'
-- 'defaultFindMessageStructureOptions'@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/8.17/find-message-structure.html>.
findMessageStructure ::
  FindMessageStructureRequest ->
  BHRequest StatusDependant FindStructureResponse
findMessageStructure =
  findMessageStructureWith defaultFindMessageStructureOptions

-- | 'findMessageStructureWith' is the fully-parameterised form of
-- 'findMessageStructure'. Every URI parameter accepted by
-- @POST /_text_structure/find_message_structure@ is exposed via
-- 'FindMessageStructureOptions' (the message-classifier subset of the
-- 'FindStructureOptions' overrides).
findMessageStructureWith ::
  FindMessageStructureOptions ->
  FindMessageStructureRequest ->
  BHRequest StatusDependant FindStructureResponse
findMessageStructureWith opts req =
  post @StatusDependant endpoint (encode req)
  where
    endpoint =
      ["_text_structure", "find_message_structure"]
        `withQueries` findMessageStructureOptionsParams opts

-- | 'findFieldStructure' samples the stored values of a field in an
-- existing index and returns the inferred structure of that field's
-- contents. Maps to @GET /_text_structure/find_field_structure@. There
-- is no request body: the @index@ and @field@ to sample are supplied as
-- URI parameters. The response reuses 'FindStructureResponse'.
--
-- This convenience sets every classifier override to its default; use
-- 'findFieldStructureWith' to pass @documents_to_sample@, format
-- overrides, @explain@, @timeout@, etc.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/8.17/find-field-structure.html>.
findFieldStructure ::
  Text ->
  Text ->
  BHRequest StatusDependant FindStructureResponse
findFieldStructure index field =
  findFieldStructureWith
    defaultFindFieldStructureOptions
      { ffsoIndex = Just index,
        ffsoField = Just field
      }

-- | 'findFieldStructureWith' is the fully-parameterised form of
-- 'findFieldStructure'. 'FindFieldStructureOptions' carries the
-- required @index@ \/ @field@ selectors plus the same classifier
-- overrides as the other text-structure endpoints.
findFieldStructureWith ::
  FindFieldStructureOptions ->
  BHRequest StatusDependant FindStructureResponse
findFieldStructureWith opts =
  get @StatusDependant endpoint
  where
    endpoint =
      ["_text_structure", "find_field_structure"]
        `withQueries` findFieldStructureOptionsParams opts

-- | 'testGrokPattern' compiles a grok pattern and matches it against
-- the supplied strings, returning the per-input match descriptors.
-- Maps to @POST /_text_structure/test_grok_pattern@. The request body
-- is 'TestGrokPatternRequest' (@grok_pattern@ + @text@ array); the
-- response is 'TestGrokPatternResponse' (@matches@ array).
--
-- Equivalent to @'testGrokPatternWith'
-- 'defaultTestGrokPatternOptions'@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/8.17/test-grok-pattern.html>.
testGrokPattern ::
  TestGrokPatternRequest ->
  BHRequest StatusDependant TestGrokPatternResponse
testGrokPattern = testGrokPatternWith defaultTestGrokPatternOptions

-- | 'testGrokPatternWith' is the fully-parameterised form of
-- 'testGrokPattern'. 'TestGrokPatternOptions' carries the
-- @ecs_compatibility@ URI parameter (@disabled@ or @v1@), which selects
-- the Elastic Common Schema grok pattern set the server prepends to the
-- caller's pattern.
testGrokPatternWith ::
  TestGrokPatternOptions ->
  TestGrokPatternRequest ->
  BHRequest StatusDependant TestGrokPatternResponse
testGrokPatternWith opts req =
  post @StatusDependant endpoint (encode req)
  where
    endpoint =
      ["_text_structure", "test_grok_pattern"]
        `withQueries` testGrokPatternOptionsParams opts

------------------------------------------------------------------------------
-- Transform API
--
-- /Transform/ is the Elasticsearch X-Pack feature that continuously or on a
-- schedule materialises a destination index from a pivot (aggregation) or a
-- latest-selection over one or more source indices. Available on
-- Elasticsearch 7.x\/8.x\/9.x (basic licence since 7.2), so every request
-- lives in the Common layer and is re-exported by every ES client module.
-- OpenSearch ships a /different/ transform plugin under
-- @\/_plugins\/_transform\/*@; calls against an OpenSearch cluster using
-- these endpoints will fail at runtime. The full surface is covered:
--

-- * @PUT /_transform/{transform_id}@ — 'putTransform' \/ 'putTransformWith'

-- * @POST /_transform/{transform_id}/_update@ — 'updateTransform' \/ 'updateTransformWith'

-- * @GET /_transform[/{transform_id}]@ — 'getTransforms' \/ 'getTransformsWith'

-- * @DELETE /_transform/{transform_id}@ — 'deleteTransform' \/ 'deleteTransformWith'

-- * @POST /_transform/{transform_id}/_start@ — 'startTransform' \/ 'startTransformWith'

-- * @POST /_transform/{transform_id}/_stop@ — 'stopTransform' \/ 'stopTransformWith'

-- * @POST /_transform/_preview@ — 'previewTransform' \/ 'previewTransformWith'

-- * @GET /_transform/_stats[/{transform_id}]@ — 'getTransformStats' \/ 'getTransformStatsWith'

-- * @GET /_transform/{transform_id}/_explain@ — 'explainTransform'

-- | 'putTransform' creates a transform. Maps to
-- @PUT /_transform/{transform_id}@ with 'defaultPutTransformOptions' and
-- returns 'PutTransformResponse' on success. The 'TransformConfig' body
-- carries the source, destination and either the @pivot@ or @latest@
-- selection. Use 'putTransformWith' to forward the @defer_validation@ or
-- @timeout@ query parameters.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/create-transform.html>.
putTransform ::
  TransformId ->
  TransformConfig ->
  BHRequest StatusDependant PutTransformResponse
putTransform tid config =
  putTransformWith tid config defaultPutTransformOptions

-- | Like 'putTransform' but accepts 'PutTransformOptions' carrying the
-- documented @defer_validation@ and @timeout@ query parameters.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/create-transform.html>.
putTransformWith ::
  TransformId ->
  TransformConfig ->
  PutTransformOptions ->
  BHRequest StatusDependant PutTransformResponse
putTransformWith (TransformId tid) config opts =
  put
    (["_transform", tid] `withQueries` putTransformOptionsParams opts)
    (encode config)

-- | 'updateTransform' updates an existing transform. Maps to
-- @POST /_transform/{transform_id}/_update@ with
-- 'defaultUpdateTransformOptions' and returns 'Acknowledged' on success.
-- Use 'updateTransformWith' to forward the @defer_validation@ query
-- parameter.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/update-transform.html>.
updateTransform ::
  TransformId ->
  TransformConfig ->
  BHRequest StatusDependant Acknowledged
updateTransform tid config =
  updateTransformWith tid config defaultUpdateTransformOptions

-- | Like 'updateTransform' but accepts 'UpdateTransformOptions' carrying
-- the documented @defer_validation@ query parameter.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/update-transform.html>.
updateTransformWith ::
  TransformId ->
  TransformConfig ->
  UpdateTransformOptions ->
  BHRequest StatusDependant Acknowledged
updateTransformWith (TransformId tid) config opts =
  post
    (["_transform", tid, "_update"] `withQueries` updateTransformOptionsParams opts)
    (encode config)

-- | 'getTransforms' lists transforms.
--
-- * @'Nothing'@  → @GET /_transform@ returns every transform on the cluster.
-- * @'Just' tid@ → @GET /_transform/{tid}@ returns just that one (as a
--   single-element @transforms@ array).
--
-- Equivalent to @'getTransformsWith' mId 'defaultGetTransformsOptions'@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/get-transform.html>.
getTransforms ::
  Maybe TransformId ->
  BHRequest StatusDependant TransformsResponse
getTransforms = (`getTransformsWith` defaultGetTransformsOptions)

-- | Like 'getTransforms' but accepts 'GetTransformsOptions' carrying the
-- documented @from@, @size@, @allow_no_match@ and @exclude_generated@
-- query parameters.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/get-transform.html>.
getTransformsWith ::
  Maybe TransformId ->
  GetTransformsOptions ->
  BHRequest StatusDependant TransformsResponse
getTransformsWith mTid opts =
  get (path `withQueries` getTransformsOptionsParams opts)
  where
    path = case mTid of
      Nothing -> ["_transform"]
      Just (TransformId tid) -> ["_transform", tid]

-- | 'deleteTransform' deletes a transform by id. Maps to
-- @DELETE /_transform/{transform_id}@ with
-- 'defaultDeleteTransformOptions' and returns 'DeleteTransformResponse'
-- on success. Use 'deleteTransformWith' to forward the @force@ or (ES 8+)
-- @delete_dest_index@ query parameters.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/delete-transform.html>.
deleteTransform ::
  TransformId ->
  BHRequest StatusDependant DeleteTransformResponse
deleteTransform = (`deleteTransformWith` defaultDeleteTransformOptions)

-- | Like 'deleteTransform' but accepts 'DeleteTransformOptions' carrying
-- the documented @force@ and @delete_dest_index@ query parameters.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/delete-transform.html>.
deleteTransformWith ::
  TransformId ->
  DeleteTransformOptions ->
  BHRequest StatusDependant DeleteTransformResponse
deleteTransformWith (TransformId tid) opts =
  delete
    (["_transform", tid] `withQueries` deleteTransformOptionsParams opts)

-- | 'startTransform' starts a transform. Maps to
-- @POST /_transform/{transform_id}/_start@ with
-- 'defaultStartTransformOptions' and returns 'Acknowledged' on success.
-- Use 'startTransformWith' to forward the @defer_validation@ or @timeout@
-- query parameters.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/start-transform.html>.
startTransform ::
  TransformId ->
  BHRequest StatusDependant Acknowledged
startTransform = (`startTransformWith` defaultStartTransformOptions)

-- | Like 'startTransform' but accepts 'StartTransformOptions' carrying
-- the documented @defer_validation@ and @timeout@ query parameters.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/start-transform.html>.
startTransformWith ::
  TransformId ->
  StartTransformOptions ->
  BHRequest StatusDependant Acknowledged
startTransformWith (TransformId tid) opts =
  post
    (["_transform", tid, "_start"] `withQueries` startTransformOptionsParams opts)
    emptyBody

-- | 'stopTransform' stops a transform. Maps to
-- @POST /_transform/{transform_id}/_stop@ with
-- 'defaultStopTransformOptions' and returns 'StopTransformResponse' on
-- success. Use 'stopTransformWith' to forward the @wait_for_checkpoint@,
-- @wait_for_completion@, @timeout@ or @allow_no_match@ query parameters.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/stop-transform.html>.
stopTransform ::
  TransformId ->
  BHRequest StatusDependant StopTransformResponse
stopTransform = (`stopTransformWith` defaultStopTransformOptions)

-- | Like 'stopTransform' but accepts 'StopTransformOptions' carrying the
-- documented @wait_for_checkpoint@, @wait_for_completion@, @timeout@ and
-- @allow_no_match@ query parameters.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/stop-transform.html>.
stopTransformWith ::
  TransformId ->
  StopTransformOptions ->
  BHRequest StatusDependant StopTransformResponse
stopTransformWith (TransformId tid) opts =
  post
    (["_transform", tid, "_stop"] `withQueries` stopTransformOptionsParams opts)
    emptyBody

-- | 'previewTransform' previews a transform configuration. Maps to
-- @POST /_transform/_preview@ with 'defaultPreviewTransformOptions' and
-- returns 'PreviewTransformResponse' on success. The 'TransformConfig'
-- body is the configuration to preview (it need not yet exist on the
-- cluster). Use 'previewTransformWith' to forward the @timeout@ query
-- parameter.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/preview-transform.html>.
previewTransform ::
  TransformConfig ->
  BHRequest StatusDependant PreviewTransformResponse
previewTransform = (`previewTransformWith` defaultPreviewTransformOptions)

-- | Like 'previewTransform' but accepts 'PreviewTransformOptions' carrying
-- the documented @timeout@ query parameter.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/preview-transform.html>.
previewTransformWith ::
  TransformConfig ->
  PreviewTransformOptions ->
  BHRequest StatusDependant PreviewTransformResponse
previewTransformWith config opts =
  post
    (["_transform", "_preview"] `withQueries` previewTransformOptionsParams opts)
    (encode config)

-- | 'getTransformStats' reports execution statistics for transforms.
--
-- * @'Nothing'@  → @GET /_transform/_stats@ returns stats for every transform.
-- * @'Just' tid@ → @GET /_transform/{tid}/_stats@ returns stats for one.
--
-- Equivalent to @'getTransformStatsWith' mId 'defaultGetTransformStatsOptions'@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/get-transform-stats.html>.
getTransformStats ::
  Maybe TransformId ->
  BHRequest StatusDependant TransformStatsResponse
getTransformStats = (`getTransformStatsWith` defaultGetTransformStatsOptions)

-- | Like 'getTransformStats' but accepts 'GetTransformStatsOptions'
-- carrying the documented @from@, @size@ and @allow_no_match@ query
-- parameters.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/get-transform-stats.html>.
getTransformStatsWith ::
  Maybe TransformId ->
  GetTransformStatsOptions ->
  BHRequest StatusDependant TransformStatsResponse
getTransformStatsWith mTid opts =
  get (path `withQueries` getTransformStatsOptionsParams opts)
  where
    path = case mTid of
      Nothing -> ["_transform", "_stats"]
      Just (TransformId tid) -> ["_transform", tid, "_stats"]

-- | 'explainTransform' explains the state of a transform. Maps to
-- @GET /_transform/{transform_id}/_explain@ and returns
-- 'TransformExplainResponse'. The explain payload is highly
-- version-dependent, so each entry carries the transform @id@ plus an
-- extras catch-all for the @state@\/@checkpointing@ sub-objects.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/explain-transform.html>.
explainTransform ::
  TransformId ->
  BHRequest StatusDependant TransformExplainResponse
explainTransform (TransformId tid) =
  get ["_transform", tid, "_explain"]

-- --------------------------------------------------------------------------
-- Migration
------------------------------------------------------------------------------

-- $migration
--
-- /Migration Assistance/ is the Elasticsearch-native tooling for planning
-- and executing major-version upgrades. Available on Elasticsearch 6.1
-- and later (deprecations) / 7.16 and later (system_features), so every
-- request lives in the Common layer and is re-exported by every ES
-- client module. The full surface is covered:
--
-- * @GET /_migration/deprecations@ (and @GET /{index}/_migration/deprecations@)
--   — list deprecations affecting the next upgrade ('getMigrationDeprecations').
-- * @GET /_migration/system_features@ — check feature upgrade status
--   ('getSystemFeatures').
-- * @POST /_migration/system_features@ — trigger the feature upgrade
--   ('upgradeSystemFeatures').

-- | 'getMigrationDeprecations' lists the deprecation warnings that would
-- affect a major-version upgrade.
--
-- * @'Nothing'@  → @GET /_migration/deprecations@ scans every index,
--   data stream, template, ILM policy and cluster\/node\/ML setting.
-- * @'Just' ix@ → @GET /{ix}/_migration/deprecations@ restricts the
--   @index_settings@ section to the supplied index (or index pattern).
--
-- The wire format is identical across ES 7.x, 8.x and 9.x, so the
-- request builder lives in the Common layer and is re-exported by every
-- ES client module.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/migration-api-deprecation.html>.
getMigrationDeprecations ::
  Maybe IndexName ->
  BHRequest StatusDependant MigrationDeprecations
getMigrationDeprecations mIndex =
  get endpoint
  where
    endpoint = case mIndex of
      Nothing -> ["_migration", "deprecations"]
      Just ix -> [unIndexName ix, "_migration", "deprecations"]

-- | 'getSystemFeatures' reports the migration status of every ES
-- feature that stores versioned configuration (async_search, enrich,
-- fleet, machine_learning, security, ...). Maps to
-- @GET /_migration/system_features@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/migration-api-feature-upgrade.html>.
getSystemFeatures ::
  BHRequest StatusDependant FeatureUpgradeStatus
getSystemFeatures = get ["_migration", "system_features"]

-- | 'upgradeSystemFeatures' kicks off the feature upgrade. Maps to
-- @POST /_migration/system_features@ and returns 'Acknowledged' on
-- success. The upgrade runs in the background; poll
-- 'getSystemFeatures' to observe progress. Mutates cluster state —
-- call deliberately.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/migration-api-feature-upgrade.html>.
upgradeSystemFeatures :: BHRequest StatusDependant Acknowledged
upgradeSystemFeatures = post ["_migration", "system_features"] emptyBody

-- --------------------------------------------------------------------------
-- Ingest Pipelines
------------------------------------------------------------------------------

-- $ingest
--
-- /Ingest pipelines/ parse, enrich and transform documents before they
-- are indexed (a chain of @processors@: @grok@, @set@, @uppercase@,
-- @script@, ...). Available on Elasticsearch 5.0 and later, and on every
-- OpenSearch version, so the request builders live in the Common layer
-- and are re-exported by every client module.
--
-- The surface covered:
--
-- * @PUT /_ingest/pipeline/{id}@ — create/update a pipeline ('putIngestPipeline').
-- * @GET /_ingest/pipeline[/{id}]@ — list one or all pipelines ('getIngestPipeline').
-- * @DELETE /_ingest/pipeline/{id}@ — delete a pipeline ('deleteIngestPipeline').
-- * @POST /_ingest/pipeline[/{id}]/_simulate@ — simulate a pipeline
--   ('simulateIngestPipeline').
-- * @GET /_ingest/processor/grok@ — list built-in grok patterns
--   ('getGrokPatterns').

-- | 'putIngestPipeline' creates or updates a single ingest pipeline by id.
-- Maps to @PUT /_ingest/pipeline/{id}@ and returns 'Acknowledged' on
-- success. The 'IngestPipeline' body is sent as-is at the top level of the
-- request — ingest, unlike ILM, does @not@ wrap the pipeline under a
-- @pipeline@ key (the same shape SLM uses).
--
-- The wire format is identical across ES 7.x, 8.x, 9.x and OS 1.x, 2.x,
-- 3.x, so the request builder lives in the Common layer and is
-- re-exported by every client module.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/put-pipeline-api.html>
-- and
-- <https://docs.opensearch.org/latest/api-reference/ingest-apis/create-pipeline/>.
putIngestPipeline ::
  PipelineId ->
  IngestPipeline ->
  BHRequest StatusDependant Acknowledged
putIngestPipeline (PipelineId pid) pipeline =
  put ["_ingest", "pipeline", pid] (encode pipeline)

-- | 'getIngestPipeline' lists ingest pipelines.
--
-- * @'Nothing'@  → @GET /_ingest/pipeline@ returns every pipeline on the cluster.
-- * @'Just' pid@ → @GET /_ingest/pipeline/{pid}@ returns just that one (as a
--   singleton list).
--
-- The response is a JSON object keyed by pipeline id, so the body decoder
-- walks the keys and folds each one into the corresponding
-- 'IngestPipelineInfo' (overriding the placeholder id set by its own
-- 'FromJSON' instance — the same trick 'getILMPolicy' uses).
--
-- /Empty-cluster handling:/ OpenSearch (and a fresh Elasticsearch
-- cluster with no built-in pipelines) responds to the list-all form
-- with HTTP 404 and an empty @{}@ body when no pipelines exist,
-- rather than a 200 with @{}@. The parser for the @'Nothing'@ branch
-- treats such a 404 as an empty list so callers see a uniform @Right
-- []@; the @'Just' pid@ branch still surfaces 404 as 'EsError' (an
-- unknown id is genuinely an error there).
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/get-pipeline-api.html>
-- and
-- <https://docs.opensearch.org/latest/api-reference/ingest-apis/get-pipeline/>.
getIngestPipeline ::
  Maybe PipelineId ->
  BHRequest StatusDependant [IngestPipelineInfo]
getIngestPipeline mPipelineId = baseReq {bhRequestParser = parser}
  where
    baseReq = unIngestPipelines <$> get @StatusDependant endpoint
    endpoint = case mPipelineId of
      Nothing -> ["_ingest", "pipeline"]
      Just (PipelineId pid) -> ["_ingest", "pipeline", pid]
    parser resp
      | isNothing mPipelineId && statusCodeIs (404, 404) resp =
          case decode (responseBody (getResponse resp)) of
            Just ips -> Right (Right (unIngestPipelines ips))
            Nothing -> bhRequestParser baseReq resp
      | otherwise = bhRequestParser baseReq resp

-- | 'deleteIngestPipeline' deletes a single ingest pipeline by id. Maps to
-- @DELETE /_ingest/pipeline/{id}@ and returns 'Acknowledged' on success.
-- Deleting a pipeline that does not exist fails with an HTTP error (hence
-- 'StatusDependant'), surfaced as an 'EsError'.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/delete-pipeline-api.html>
-- and
-- <https://docs.opensearch.org/latest/api-reference/ingest-apis/delete-pipeline/>.
deleteIngestPipeline ::
  PipelineId ->
  BHRequest StatusDependant Acknowledged
deleteIngestPipeline (PipelineId pid) =
  delete ["_ingest", "pipeline", pid]

-- | 'simulateIngestPipeline' runs the supplied documents through an ingest
-- pipeline without indexing anything, returning the per-document
-- transformed results. Maps to @POST /_ingest/pipeline[\/{id}]\/_simulate@.
--
-- The first argument is the pipeline id:
--
-- * @'Nothing'@  → @POST /_ingest/pipeline/_simulate@ runs the supplied
--   'IngestPipeline' body.
-- * @'Just' pid@ → @POST /_ingest/pipeline/{pid}/_simulate@ runs the
--   pipeline /stored/ at @pid@; the supplied 'IngestPipeline' argument is
--   serialized into the request body's @pipeline@ field but Elasticsearch
--   /ignores/ it in favor of the stored pipeline. To simulate an inline
--   pipeline body, pass 'Nothing' for the first argument.
--
-- The public API forces the caller to supply an 'IngestPipeline' (per the
-- bead spec) even though it is functionally ignored when a 'Just pid' is
-- given. This is deliberate (the bead locks the signature) and tracked as
-- a follow-up: a @simulateIngestPipelineById :: MonadBH m => PipelineId ->
-- [Value] -> m SimulateResult@ variant would not need a body argument at
-- all.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/simulate-pipeline-api.html>
-- and
-- <https://docs.opensearch.org/latest/api-reference/ingest-apis/run-pipeline/>.
simulateIngestPipeline ::
  Maybe PipelineId ->
  IngestPipeline ->
  [Value] ->
  BHRequest StatusDependant SimulateResult
simulateIngestPipeline mPipelineId pipeline docs =
  post @StatusDependant endpoint (encode body)
  where
    endpoint = case mPipelineId of
      Nothing -> ["_ingest", "pipeline", "_simulate"]
      Just (PipelineId pid) -> ["_ingest", "pipeline", pid, "_simulate"]
    body = SimulateIngestPipelineRequest (Just pipeline) docs

-- | 'getGrokPatterns' lists the built-in grok patterns shipped with the
-- grok ingest processor. Maps to @GET /_ingest/processor/grok@ and
-- returns a 'GrokPatterns' (a flat map from pattern name to its grok
-- definition). Useful for inspecting the patterns your
-- @grok@-processor pipelines may reference.
--
-- The endpoint also accepts the optional @ecs_compatibility@ and @s@
-- (sort) query parameters; selecting ECS-compatible patterns and
-- sorting the response by key respectively. They are not yet exposed
-- here — tracked as a follow-up.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/grok-processor.html#grok-processor-get>.
getGrokPatterns ::
  BHRequest StatusDependant GrokPatterns
getGrokPatterns = get ["_ingest", "processor", "grok"]

-- --------------------------------------------------------------------------
-- Async Search
-----------------------------------------------------------------------------

-- | 'deleteAsyncSearch' deletes an async search result and its context by id.
-- Maps to @DELETE /_async_search/{id}@ and returns 'Acknowledged' on success.
--
-- Available on Elasticsearch 7.7 and later, so the request lives in the
-- Common layer and is re-exported by every ES client module.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/async-search.html#delete-async-search>.
deleteAsyncSearch ::
  AsyncSearchId ->
  BHRequest StatusIndependant Acknowledged
deleteAsyncSearch (AsyncSearchId i) =
  delete ["_async_search", i]

-- | 'submitAsyncSearch' submits a 'Search' for asynchronous execution.
-- Maps to @POST /_async_search@ and returns immediately with an id and
-- partial results (see 'AsyncSearchResult'). Poll the returned id with
-- @GET /_async_search/{id}@ until 'asyncSearchIsRunning' is @False@.
--
-- Equivalent to @'submitAsyncSearchWith' 'defaultAsyncSearchSubmitOptions'@
-- — it forwards no URI parameters, matching the documented defaults
-- (@wait_for_completion=false@, @keep_on_completion=false@,
-- @keep_alive=5d@).
--
-- Available on Elasticsearch 7.7 and later, so the request lives in the
-- Common layer and is re-exported by every ES client module.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/async-search.html#submit-async-search>.
submitAsyncSearch ::
  (FromJSON a) =>
  Search ->
  BHRequest StatusDependant (AsyncSearchResult a)
submitAsyncSearch = submitAsyncSearchWith defaultAsyncSearchSubmitOptions

-- | Like 'submitAsyncSearch' but accepts 'AsyncSearchSubmitOptions'
-- carrying the URI parameters documented for @POST /_async_search@:
-- @wait_for_completion@ (set @'Just' 'True'@ to block),
-- @keep_on_completion@ (set @'Just' 'True'@ to persist on completion),
-- @keep_alive@ (retention window, as a typed 'KeepAlive' value),
-- @batched_reduce_size@, and @ccs_minimize_roundtrips@.
-- 'defaultAsyncSearchSubmitOptions' makes this byte-for-byte equivalent
-- to 'submitAsyncSearch'.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/async-search.html#submit-async-search>.
submitAsyncSearchWith ::
  (FromJSON a) =>
  AsyncSearchSubmitOptions ->
  Search ->
  BHRequest StatusDependant (AsyncSearchResult a)
submitAsyncSearchWith opts search =
  post (["_async_search"] `withQueries` asyncSearchSubmitOptionsParams opts) (encode search)

------------------------------------------------------------------------------
-- Enrich policy API
--

-- * @PUT /_enrich/policy/{policy_id}@ — 'putEnrichPolicy'

-- * @DELETE /_enrich/policy/{policy_id}@ — 'deleteEnrichPolicy'

-- * @GET /_enrich/policy[/{policy_id}]@ — 'getEnrichPolicy'

-- * @POST /_enrich/policy/{policy_id}/_execute@ — 'executeEnrichPolicy'

-- * @GET /_enrich/_execute_stats@ — 'getEnrichExecuteStats'

-- * @GET /_enrich/policy/{policy_id}/_execute_stats@ — 'getEnrichPolicyExecuteStats'

-- | 'putEnrichPolicy' creates or replaces an enrich policy by id. Maps to
-- @PUT /_enrich/policy/{policy_id}@ and returns 'Acknowledged' on
-- success. The 'EnrichPolicy' body is encoded verbatim — its top-level
-- object key is the policy type (@match@, @geo_match@, @range@) and its
-- value is the 'EnrichPolicyConfig'.
--
-- Enrich is part of the free (basic) X-Pack tier and is available across
-- ES 7.x\/8.x\/9.x with the same wire surface, so the request lives in
-- the Common layer and is re-exported by every ES client module.
-- OpenSearch does not implement the enrich API.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/put-enrich-policy-api.html>.
putEnrichPolicy ::
  EnrichPolicyId ->
  EnrichPolicy ->
  BHRequest StatusDependant Acknowledged
putEnrichPolicy (EnrichPolicyId pid) policy =
  put ["_enrich", "policy", pid] (encode policy)

-- | 'getEnrichPolicy' lists enrich policies.
--
-- * @'Nothing'@  → @GET /_enrich/policy@ returns every policy on the cluster.
-- * @'Just' pid@ → @GET /_enrich/policy/{pid}@ returns just that one (as
--   a singleton list inside the 'EnrichPoliciesResponse').
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/get-enrich-policy-api.html>.
getEnrichPolicy ::
  Maybe EnrichPolicyId ->
  BHRequest StatusDependant EnrichPoliciesResponse
getEnrichPolicy mPolicyId =
  get @StatusDependant endpoint
  where
    endpoint = case mPolicyId of
      Nothing -> ["_enrich", "policy"]
      Just (EnrichPolicyId pid) -> ["_enrich", "policy", pid]

-- | 'deleteEnrichPolicy' deletes a single enrich policy by id. Maps to
-- @DELETE /_enrich/policy/{policy_id}@ and returns 'Acknowledged' on
-- success.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/delete-enrich-policy-api.html>.
deleteEnrichPolicy ::
  EnrichPolicyId ->
  BHRequest StatusDependant Acknowledged
deleteEnrichPolicy (EnrichPolicyId pid) =
  delete ["_enrich", "policy", pid]

-- | 'executeEnrichPolicy' triggers a synchronous execution of an enrich
-- policy — ES creates (or refreshes) the @.enrich-{policy_id}-{version}@
-- follower index used by @enrich@ processor queries. Maps to
-- @POST /_enrich/policy/{policy_id}/_execute@ with
-- @wait_for_completion@ defaulted to @true@ by the server, returning
-- 'Acknowledged' on success. Use 'executeEnrichPolicyWith' to set
-- @wait_for_completion=false@ (the response then becomes a task handle,
-- which is outside the typed envelope and must be decoded separately).
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/execute-enrich-policy-api.html>.
executeEnrichPolicy ::
  EnrichPolicyId ->
  BHRequest StatusDependant Acknowledged
executeEnrichPolicy pid =
  executeEnrichPolicyWith pid defaultEnrichExecuteOptions

-- | Like 'executeEnrichPolicy' but accepts 'EnrichExecuteOptions' carrying
-- the documented URI parameter @wait_for_completion@.
-- 'defaultEnrichExecuteOptions' makes this byte-for-byte equivalent to
-- 'executeEnrichPolicy'.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/execute-enrich-policy-api.html>.
executeEnrichPolicyWith ::
  EnrichPolicyId ->
  EnrichExecuteOptions ->
  BHRequest StatusDependant Acknowledged
executeEnrichPolicyWith (EnrichPolicyId pid) opts =
  post
    (["_enrich", "policy", pid, "_execute"] `withQueries` enrichExecuteOptionsParams opts)
    emptyBody

-- | 'getEnrichExecuteStats' reports the executor pool and per-policy
-- execution statistics across the cluster. Maps to
-- @GET /_enrich/_execute_stats@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/enrich-stats-api.html>.
getEnrichExecuteStats ::
  BHRequest StatusDependant EnrichExecuteStatsResponse
getEnrichExecuteStats =
  get ["_enrich", "_execute_stats"]

-- | 'getEnrichPolicyExecuteStats' reports the executor pool and execution
-- statistics for a single enrich policy. Maps to
-- @GET /_enrich/policy/{policy_id}/_execute_stats@ (the per-policy variant
-- added in ES 7.16). The response shape is identical to
-- 'getEnrichExecuteStats' but the @execute_stats@ array is filtered to
-- the requested policy.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/enrich-stats-api.html>.
getEnrichPolicyExecuteStats ::
  EnrichPolicyId ->
  BHRequest StatusDependant EnrichExecuteStatsResponse
getEnrichPolicyExecuteStats (EnrichPolicyId pid) =
  get ["_enrich", "policy", pid, "_execute_stats"]

------------------------------------------------------------------------------
-- Autoscaling API
--
-- The autoscaling endpoints live in the X-Pack tier and are designed for
-- indirect use by cloud orchestrators (Elasticsearch Service, ECE, ECK).
-- They share the ES 7.x\/8.x\/9.x wire surface, so the requests live in the
-- Common layer alongside SLM\/Enrich. Endpoint reference (see
-- "Database.Bloodhound.Internal.Versions.Common.Types.Autoscaling" for the
-- request/response shapes):

-- * @PUT /_autoscaling/policy/{name}@ — 'putAutoscalingPolicy'

-- * @GET /_autoscaling/policy/{name}@ — 'getAutoscalingPolicy'

-- * @DELETE /_autoscaling/policy/{name}@ — 'deleteAutoscalingPolicy'

-- * @GET /_autoscaling/capacity@ — 'getAutoscalingCapacity'

-- | 'putAutoscalingPolicy' creates or replaces an autoscaling policy by name.
-- Maps to @PUT /_autoscaling/policy/{name}@ and returns 'Acknowledged' on
-- success. The 'AutoscalingPolicy' body is encoded verbatim — its top level
-- is a @roles@ array and a @deciders@ object.
--
-- Autoscaling is part of the X-Pack tier and is available across ES
-- 7.x\/8.x\/9.x with the same wire surface, so the request lives in the Common
-- layer and is re-exported by every ES client module. OpenSearch does not
-- implement the autoscaling API.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/autoscaling-put-autoscaling-policy.html>.
putAutoscalingPolicy ::
  AutoscalingPolicyName ->
  AutoscalingPolicy ->
  BHRequest StatusDependant Acknowledged
putAutoscalingPolicy (AutoscalingPolicyName name) policy =
  put ["_autoscaling", "policy", name] (encode policy)

-- | 'getAutoscalingPolicy' retrieves a single autoscaling policy by name.
-- Maps to @GET /_autoscaling/policy/{name}@ and returns the bare
-- 'AutoscalingPolicy' body (ES returns @roles@ and @deciders@ directly,
-- without an envelope).
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/autoscaling-get-autoscaling-policy.html>.
getAutoscalingPolicy ::
  AutoscalingPolicyName ->
  BHRequest StatusDependant AutoscalingPolicy
getAutoscalingPolicy (AutoscalingPolicyName name) =
  get ["_autoscaling", "policy", name]

-- | 'deleteAutoscalingPolicy' deletes a single autoscaling policy by name.
-- Maps to @DELETE /_autoscaling/policy/{name}@ and returns 'Acknowledged' on
-- success.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/autoscaling-delete-autoscaling-policy.html>.
deleteAutoscalingPolicy ::
  AutoscalingPolicyName ->
  BHRequest StatusDependant Acknowledged
deleteAutoscalingPolicy (AutoscalingPolicyName name) =
  delete ["_autoscaling", "policy", name]

-- | 'getAutoscalingCapacity' reports the current autoscaling capacity for
-- every configured policy — the storage\/memory ES says each tier needs
-- versus what it currently has, plus the nodes and per-decider results used
-- to derive it. Maps to @GET /_autoscaling/capacity@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/autoscaling-get-autoscaling-capacity.html>.
getAutoscalingCapacity ::
  BHRequest StatusDependant AutoscalingCapacity
getAutoscalingCapacity =
  get ["_autoscaling", "capacity"]

------------------------------------------------------------------------------
-- Security API (/_security/*)
--
-- X-Pack Security covers users, roles, role mappings and API keys. The
-- surface is shared across ES 7.x\/8.x\/9.x, so these live in the Common
-- layer (OpenSearch implements security via a separate plugin at
-- @/_plugins/_security@, not covered here). Privileges and OIDC/SAML tokens
-- are tracked in follow-up beads.

------------------------------------------------------------------------------
-- Security — roles (/_security/role/*)

-- | 'putRole' creates or replaces a role by name. Maps to
-- @PUT /_security/role/{name}@ and returns 'RoleCreatedResponse'.
putRole :: RoleName -> Role -> BHRequest StatusDependant RoleCreatedResponse
putRole (RoleName name) role =
  put ["_security", "role", name] (encode role)

-- | 'getRoles' lists every role on the cluster. Maps to
-- @GET /_security/role@.
getRoles :: BHRequest StatusDependant RolesListResponse
getRoles = get ["_security", "role"]

-- | 'getRole' fetches a single role by name. Maps to
-- @GET /_security/role/{name}@; the response is a singleton
-- 'RolesListResponse' (empty if the role does not exist).
getRole :: RoleName -> BHRequest StatusDependant RolesListResponse
getRole (RoleName name) = get ["_security", "role", name]

-- | 'deleteRole' deletes a role by name. Maps to
-- @DELETE /_security/role/{name}@.
deleteRole ::
  RoleName -> BHRequest StatusDependant RoleDeletedResponse
deleteRole (RoleName name) = delete ["_security", "role", name]

-- | 'clearRoleCache' evicts a role from the security cache. Maps to
-- @POST /_security/role/{name}/_clear_cache@.
clearRoleCache ::
  RoleName -> BHRequest StatusDependant ClearRoleCacheResponse
clearRoleCache (RoleName name) =
  post ["_security", "role", name, "_clear_cache"] emptyBody

------------------------------------------------------------------------------
-- Security — role mappings (/_security/role_mapping/*)

-- | 'putRoleMapping' creates or replaces a role mapping. Maps to
-- @PUT /_security/role_mapping/{name}@.
putRoleMapping ::
  RoleMappingName ->
  RoleMapping ->
  BHRequest StatusDependant Acknowledged
putRoleMapping (RoleMappingName name) rm =
  put ["_security", "role_mapping", name] (encode rm)

-- | 'getRoleMappings' lists every role mapping.
getRoleMappings ::
  BHRequest StatusDependant RoleMappingsListResponse
getRoleMappings = get ["_security", "role_mapping"]

-- | 'getRoleMapping' fetches a single role mapping by name.
getRoleMapping ::
  RoleMappingName ->
  BHRequest StatusDependant RoleMappingsListResponse
getRoleMapping (RoleMappingName name) = get ["_security", "role_mapping", name]

-- | 'deleteRoleMapping' deletes a role mapping by name.
deleteRoleMapping :: RoleMappingName -> BHRequest StatusDependant Acknowledged
deleteRoleMapping (RoleMappingName name) =
  delete ["_security", "role_mapping", name]

------------------------------------------------------------------------------
-- Security — users (/_security/user/*)

-- | 'putUser' creates or updates a user. Maps to
-- @PUT /_security/user/{username}@ and returns 'UserCreatedResponse'.
putUser ::
  UserName ->
  UserCreateBody ->
  BHRequest StatusDependant UserCreatedResponse
putUser (UserName username) body =
  put ["_security", "user", username] (encode body)

-- | 'getUsers' lists every user. Maps to @GET /_security/user@.
getUsers :: BHRequest StatusDependant UsersListResponse
getUsers = get ["_security", "user"]

-- | 'getUser' fetches a single user by username.
getUser :: UserName -> BHRequest StatusDependant UsersListResponse
getUser (UserName username) = get ["_security", "user", username]

-- | 'deleteUser' deletes a user by username. Maps to
-- @DELETE /_security/user/{username}@.
deleteUser ::
  UserName -> BHRequest StatusDependant UserDeletedResponse
deleteUser (UserName username) = delete ["_security", "user", username]

-- | 'enableUser' enables a user. Maps to
-- @PUT /_security/user/{username}/_enable@.
enableUser :: UserName -> BHRequest StatusDependant Acknowledged
enableUser (UserName username) =
  put ["_security", "user", username, "_enable"] emptyBody

-- | 'disableUser' disables a user. Maps to
-- @PUT /_security/user/{username}/_disable@.
disableUser :: UserName -> BHRequest StatusDependant Acknowledged
disableUser (UserName username) =
  put ["_security", "user", username, "_disable"] emptyBody

-- | 'changeUserPassword' changes a user's password. Maps to
-- @POST /_security/user/{username}/_password@.
changeUserPassword ::
  UserName ->
  ChangePasswordBody ->
  BHRequest StatusDependant Acknowledged
changeUserPassword (UserName username) body =
  post ["_security", "user", username, "_password"] (encode body)

-- | 'userHasPrivileges' checks the privileges held by a named user.
-- Maps to @POST /_security/user/{username}/_has_privileges@.
userHasPrivileges ::
  UserName ->
  HasPrivilegesRequest ->
  BHRequest StatusDependant HasPrivilegesResponse
userHasPrivileges (UserName username) req =
  post ["_security", "user", username, "_has_privileges"] (encode req)

-- | 'selfHasPrivileges' checks the privileges held by the current user.
-- Maps to @POST /_security/user/_has_privileges@.
selfHasPrivileges ::
  HasPrivilegesRequest ->
  BHRequest StatusDependant HasPrivilegesResponse
selfHasPrivileges req =
  post ["_security", "user", "_has_privileges"] (encode req)

-- | 'getUserPrivileges' lists the current user's effective privileges.
-- Maps to @GET /_security/user/_privileges@.
getUserPrivileges :: BHRequest StatusDependant UserPrivileges
getUserPrivileges = get ["_security", "user", "_privileges"]

------------------------------------------------------------------------------
-- Security — API keys (/_security/api_key)

-- | 'createApiKey' mints a new API key. Maps to @POST /_security/api_key@.
createApiKey ::
  CreateApiKeyRequest ->
  BHRequest StatusDependant ApiKeyCreatedResponse
createApiKey req = post ["_security", "api_key"] (encode req)

-- | 'grantApiKey' mints an API key on behalf of another principal. Maps
-- to @POST /_security/api_key/grant@.
grantApiKey ::
  GrantApiKeyRequest ->
  BHRequest StatusDependant ApiKeyCreatedResponse
grantApiKey req = post ["_security", "api_key", "grant"] (encode req)

-- | 'getApiKey' retrieves API keys matching the filter. Maps to
-- @GET /_security/api_key@. The request is sent as a body (ES accepts a
-- GET body here), so this uses 'getWithBody'.
getApiKey ::
  RetrieveApiKeyRequest ->
  BHRequest StatusDependant ApiKeyRetrievalResponse
getApiKey req = getWithBody ["_security", "api_key"] (encode req)

-- | 'invalidateApiKey' revokes API keys matching the filter. Maps to
-- @DELETE /_security/api_key@ with a body, so this uses 'deleteWithBody'.
invalidateApiKey ::
  InvalidateApiKeyRequest ->
  BHRequest StatusDependant InvalidateApiKeyResponse
invalidateApiKey req =
  deleteWithBody ["_security", "api_key"] (encode req)

-- | 'updateApiKey' updates a key's role descriptors and/or metadata.
-- Maps to @PUT /_security/api_key/{id}@.
updateApiKey ::
  ApiKeyId ->
  UpdateApiKeyRequest ->
  BHRequest StatusDependant Acknowledged
updateApiKey (ApiKeyId apiKeyId) body =
  put ["_security", "api_key", apiKeyId] (encode body)

------------------------------------------------------------------------------
-- Security — API key query (ES 7.16+ / 8.x: /_security/_query/api_key)

-- | 'queryApiKey' lists API keys via the Query-DSL paginated endpoint.
-- Maps to @POST /_security/_query/api_key@. Unlike 'getApiKey' (a fixed
-- set of scalar filters on the ES 7.x surface), this endpoint accepts a
-- Query-DSL @query@ plus pagination \/ sort \/ aggregations and does not
-- exist on ES 7.x.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/8.17/security-api-query-api-key.html>.
queryApiKey ::
  QueryApiKeyRequest ->
  BHRequest StatusDependant QueryApiKeyResponse
queryApiKey req = post ["_security", "_query", "api_key"] (encode req)

-- | Variant of 'queryApiKey' that also sets the @with_limited_by@,
-- @with_profile_uid@, and @typed_keys@ URI parameters. Calling with
-- 'defaultQueryApiKeyOptions' is byte-for-byte identical to 'queryApiKey'.
queryApiKeyWith ::
  QueryApiKeyRequest ->
  QueryApiKeyOptions ->
  BHRequest StatusDependant QueryApiKeyResponse
queryApiKeyWith req opts =
  post
    (["_security", "_query", "api_key"] `withQueries` queryApiKeyOptionsParams opts)
    (encode req)

-- | URI parameters for 'queryApiKeyWith'.
data QueryApiKeyOptions = QueryApiKeyOptions
  { qkoWithLimitedBy :: Maybe Bool,
    qkoWithProfileUid :: Maybe Bool,
    qkoTypedKeys :: Maybe Bool
  }

-- | All-'Nothing' options — produces no query string.
defaultQueryApiKeyOptions :: QueryApiKeyOptions
defaultQueryApiKeyOptions =
  QueryApiKeyOptions
    { qkoWithLimitedBy = Nothing,
      qkoWithProfileUid = Nothing,
      qkoTypedKeys = Nothing
    }

-- | Render 'QueryApiKeyOptions' as @(key, value)@ pairs for 'withQueries'.
queryApiKeyOptionsParams :: QueryApiKeyOptions -> [(Text, Maybe Text)]
queryApiKeyOptionsParams opts =
  catMaybes
    [ ("with_limited_by",) . Just . boolQP <$> qkoWithLimitedBy opts,
      ("with_profile_uid",) . Just . boolQP <$> qkoWithProfileUid opts,
      ("typed_keys",) . Just . boolQP <$> qkoTypedKeys opts
    ]

------------------------------------------------------------------------------
-- Security — authentication (/_security/_authenticate)

-- | 'authenticate' returns information about the credentials the request
-- was made with. Maps to @GET /_security/_authenticate@. When the request
-- carries an API key (@Authorization: ApiKey <encoded>@), the response
-- describes that API key — this is how a caller validates a token (there
-- is no separate @/_security/api_key/_authenticate@ endpoint).
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/8.17/security-api-authenticate.html>.
authenticate :: BHRequest StatusDependant AuthenticateResponse
authenticate = get ["_security", "_authenticate"]

-- | 'whoami' returns the identity of the current authenticated user
-- (ES 7.16+). Maps to @GET /_security/_whoami@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/security-api-whoami.html>.
whoami :: BHRequest StatusDependant WhoAmIResponse
whoami = get ["_security", "_whoami"]

-- | 'logout' logs out of a SAML/OIDC SSO token. Maps to
-- @POST /_security/_logout@ with the 'LogoutRequest' body (the SSO access
-- token to invalidate); 'loChanged' is @false@ when no token matched.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/security-api-logout.html>.
logout ::
  LogoutRequest ->
  BHRequest StatusDependant LogoutResponse
logout req =
  post ["_security", "_logout"] (encode req)

------------------------------------------------------------------------------
-- Security — service accounts (/_security/service/*)
--
-- Service accounts are reserved principals (@<namespace>/<service>@, e.g.
-- @elastic/fleet-server@) authenticated by service tokens. The ES 7.17
-- wire path is @/_security/service*@ with a @credential@ segment for
-- tokens. Service tokens never expire.

-- | 'getServiceAccounts' lists every service account. Maps to
-- @GET /_security/service@.
getServiceAccounts ::
  BHRequest StatusDependant ServiceAccountsListResponse
getServiceAccounts = get ["_security", "service"]

-- | 'getServiceAccountsInNamespace' lists the service accounts in a
-- namespace. Maps to @GET /_security/service/{namespace}@.
getServiceAccountsInNamespace ::
  ServiceAccountNamespace ->
  BHRequest StatusDependant ServiceAccountsListResponse
getServiceAccountsInNamespace (ServiceAccountNamespace namespace) =
  get ["_security", "service", namespace]

-- | 'getServiceAccount' retrieves a single service account. Maps to
-- @GET /_security/service/{namespace}/{service}@.
getServiceAccount ::
  ServiceAccountNamespace ->
  ServiceAccountService ->
  BHRequest StatusDependant ServiceAccountsListResponse
getServiceAccount
  (ServiceAccountNamespace namespace)
  (ServiceAccountService service) =
    get ["_security", "service", namespace, service]

-- | 'createServiceToken' creates a service account token. Maps to
-- @POST /_security/service/{namespace}/{service}/credential/token@ when
-- the token name is 'Nothing' (ES assigns a random name), or
-- @.../credential/token/{token_name}@ when it is 'Just'. The secret token
-- @value@ is returned exactly once in the response.
createServiceToken ::
  ServiceAccountNamespace ->
  ServiceAccountService ->
  Maybe ServiceTokenName ->
  BHRequest StatusDependant CreateServiceTokenResponse
createServiceToken
  (ServiceAccountNamespace namespace)
  (ServiceAccountService service)
  maybeTokenName =
    post endpoint emptyBody
    where
      endpoint =
        case maybeTokenName of
          Nothing -> ["_security", "service", namespace, service, "credential", "token"]
          Just (ServiceTokenName tokenName) ->
            ["_security", "service", namespace, service, "credential", "token", tokenName]

-- | 'getServiceCredentials' lists the credentials (tokens) of a service
-- account. Maps to
-- @GET /_security/service/{namespace}/{service}/credential@. Secret token
-- values are not returned; only the index-backed @tokens@ names and the
-- file-backed @nodes_credentials@.
getServiceCredentials ::
  ServiceAccountNamespace ->
  ServiceAccountService ->
  BHRequest StatusDependant GetServiceCredentialsResponse
getServiceCredentials
  (ServiceAccountNamespace namespace)
  (ServiceAccountService service) =
    get ["_security", "service", namespace, service, "credential"]

-- | 'deleteServiceToken' deletes a service account token. Maps to
-- @DELETE /_security/service/{namespace}/{service}/credential/token/{token_name}@.
-- A missing token yields 404 with @found=false@ (surfaced via
-- 'StatusDependant').
deleteServiceToken ::
  ServiceAccountNamespace ->
  ServiceAccountService ->
  ServiceTokenName ->
  BHRequest StatusDependant ServiceTokenDeletedResponse
deleteServiceToken
  (ServiceAccountNamespace namespace)
  (ServiceAccountService service)
  (ServiceTokenName tokenName) =
    delete ["_security", "service", namespace, service, "credential", "token", tokenName]

------------------------------------------------------------------------------
-- Security — SSO / token (/_security/oauth2, /_security/oidc, /_security/saml)

-- | 'getToken' mints an OAuth2 bearer token. Maps to
-- @POST /_security/oauth2/token@. The grant type (password, _kerberos,
-- client_credentials, refresh_token) selects which fields of the
-- 'GetTokenRequest' are consulted.
getToken ::
  GetTokenRequest ->
  BHRequest StatusDependant GetTokenResponse
getToken req = post ["_security", "oauth2", "token"] (encode req)

-- | 'invalidateToken' revokes an access/refresh token (or every token
-- for a realm/user). Maps to @DELETE /_security/oauth2/token@ with a
-- body, so this uses 'deleteWithBody'.
invalidateToken ::
  InvalidateTokenRequest ->
  BHRequest StatusDependant InvalidateTokenResponse
invalidateToken req =
  deleteWithBody ["_security", "oauth2", "token"] (encode req)

-- | 'prepareSamlAuthentication' mints a SAML @<AuthnRequest>@ redirect
-- URL. Maps to @POST /_security/saml/prepare@. The returned 'sppId'
-- must be echoed back via 'sarIds' at authenticate time.
prepareSamlAuthentication ::
  SamlPrepareRequest ->
  BHRequest StatusDependant SamlPrepareResponse
prepareSamlAuthentication req =
  post ["_security", "saml", "prepare"] (encode req)

-- | 'authenticateSaml' exchanges a SAML @<Response>@ for an ES access
-- and refresh token. Maps to @POST /_security/saml/authenticate@.
authenticateSaml ::
  SamlAuthenticateRequest ->
  BHRequest StatusDependant SsoTokenResponse
authenticateSaml req =
  post ["_security", "saml", "authenticate"] (encode req)

-- | 'logoutSaml' invalidates a SAML-issued token pair. Maps to
-- @POST /_security/saml/logout@.
logoutSaml ::
  SamlLogoutRequest ->
  BHRequest StatusDependant SsoRedirectResponse
logoutSaml req =
  post ["_security", "saml", "logout"] (encode req)

-- | 'prepareOidcAuthentication' mints an OpenID Connect authentication
-- request URL. Maps to @POST /_security/oidc/prepare@. The returned
-- state and nonce must be echoed back at authenticate time.
prepareOidcAuthentication ::
  OidcPrepareRequest ->
  BHRequest StatusDependant OidcPrepareResponse
prepareOidcAuthentication req =
  post ["_security", "oidc", "prepare"] (encode req)

-- | 'authenticateOidc' exchanges an OpenID Connect authentication
-- response for an ES access and refresh token. Maps to
-- @POST /_security/oidc/authenticate@.
authenticateOidc ::
  OidcAuthenticateRequest ->
  BHRequest StatusDependant SsoTokenResponse
authenticateOidc req =
  post ["_security", "oidc", "authenticate"] (encode req)

-- | 'logoutOidc' invalidates an OIDC-issued token pair. Maps to
-- @POST /_security/oidc/logout@.
logoutOidc ::
  OidcLogoutRequest ->
  BHRequest StatusDependant SsoRedirectResponse
logoutOidc req =
  post ["_security", "oidc", "logout"] (encode req)

------------------------------------------------------------------------------
-- Security — application privileges (/_security/privilege/*)
--
-- Application privileges /as definitions/: each privilege is identified by
-- an @<application>@\/@<privilege>@ pair and grants a list of @actions@
-- (distinct from the role-binding application privilege, which references
-- privilege names + resources). The wire path is
-- @/_security/privilege/<application>[/<privilege>]@, with a trailing
-- @_clear_cache@ segment for cache eviction. ES echoes PUT/DELETE results
-- in a two-level @{app:{name:leaf}}@ envelope, projected to the leaf by
-- the response types below.

-- | 'putPrivilege' creates or updates a single application privilege. Maps
-- to @PUT /_security/privilege/{application}/{privilege}@ with an
-- 'ApplicationPrivilegeDefinition' body. Returns 'PrivilegeCreatedResponse'
-- (@created@ is @false@ when an existing privilege was updated).
putPrivilege ::
  ApplicationName ->
  ApplicationPrivilegeName ->
  ApplicationPrivilegeDefinition ->
  BHRequest StatusDependant PrivilegeCreatedResponse
putPrivilege
  (ApplicationName application)
  (ApplicationPrivilegeName privilege)
  definition =
    put ["_security", "privilege", application, privilege] (encode definition)

-- | 'getPrivileges' lists every application privilege. Maps to
-- @GET /_security/privilege@.
getPrivileges :: BHRequest StatusDependant PrivilegesListResponse
getPrivileges = get ["_security", "privilege"]

-- | 'getPrivilegesInApplication' lists every privilege for one
-- application. Maps to @GET /_security/privilege/{application}@.
getPrivilegesInApplication ::
  ApplicationName ->
  BHRequest StatusDependant PrivilegesListResponse
getPrivilegesInApplication (ApplicationName application) =
  get ["_security", "privilege", application]

-- | 'getPrivilege' fetches a single privilege. Maps to
-- @GET /_security/privilege/{application}/{privilege}@; the response is a
-- singleton 'PrivilegesListResponse' (a 404 is surfaced via
-- 'StatusDependant' when the privilege is undefined).
getPrivilege ::
  ApplicationName ->
  ApplicationPrivilegeName ->
  BHRequest StatusDependant PrivilegesListResponse
getPrivilege
  (ApplicationName application)
  (ApplicationPrivilegeName privilege) =
    get ["_security", "privilege", application, privilege]

-- | 'deletePrivilege' deletes a single application privilege. Maps to
-- @DELETE /_security/privilege/{application}/{privilege}@.
deletePrivilege ::
  ApplicationName ->
  ApplicationPrivilegeName ->
  BHRequest StatusDependant PrivilegeDeletedResponse
deletePrivilege
  (ApplicationName application)
  (ApplicationPrivilegeName privilege) =
    delete ["_security", "privilege", application, privilege]

-- | 'clearPrivilegeCache' evicts application privileges from the native
-- privilege cache. Maps to
-- @POST /_security/privilege/{applications}/_clear_cache@ where
-- @applications@ is the comma-joined list of application names; pass
-- @"*"@ (via 'IsString') to clear every application.
clearPrivilegeCache ::
  NonEmpty ApplicationName ->
  BHRequest StatusDependant ClearPrivilegeCacheResponse
clearPrivilegeCache applications =
  post ["_security", "privilege", applicationsSpec, "_clear_cache"] emptyBody
  where
    applicationsSpec =
      T.intercalate "," (map unApplicationName (toList applications))

------------------------------------------------------------------------------
-- Fleet API
--
-- The Fleet endpoints are experimental and exist so Fleet Server can use
-- Elasticsearch as its data store. They share the ES 7.x\/8.x\/9.x wire
-- surface, so the requests live in the Common layer alongside SLM\/Enrich.
-- Endpoint reference (see
-- "Database.Bloodhound.Internal.Versions.Common.Types.Fleet" for the
-- request/response shapes):
--

-- * @GET /{index}/_fleet/global_checkpoints@ — 'getFleetGlobalCheckpoints'

-- * @POST /{index}/_fleet/_fleet_search@ — 'fleetSearch'

-- * @POST /{index}/_fleet/_fleet_msearch@ — 'fleetMultiSearch'

-- | 'getFleetGlobalCheckpoints' returns the current global checkpoints for
-- a single index. Maps to @GET /{index}/_fleet/global_checkpoints@ with
-- 'defaultFleetGlobalCheckpointsOptions' (returns immediately). Use
-- 'getFleetGlobalCheckpointsWith' to poll until the checkpoints advance.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/get-global-checkpoints.html>.
getFleetGlobalCheckpoints ::
  IndexName ->
  BHRequest StatusDependant FleetGlobalCheckpointsResponse
getFleetGlobalCheckpoints indexName =
  getFleetGlobalCheckpointsWith indexName defaultFleetGlobalCheckpointsOptions

-- | Like 'getFleetGlobalCheckpoints' but accepts 'FleetGlobalCheckpointsOptions'.
-- Set 'fgcoWaitForAdvance' to poll until the checkpoints advance past
-- 'fgcoCheckpoints' (up to 'fgcoTimeout').
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/get-global-checkpoints.html>.
getFleetGlobalCheckpointsWith ::
  IndexName ->
  FleetGlobalCheckpointsOptions ->
  BHRequest StatusDependant FleetGlobalCheckpointsResponse
getFleetGlobalCheckpointsWith indexName opts =
  get
    ( [unIndexName indexName, "_fleet", "global_checkpoints"]
        `withQueries` fleetGlobalCheckpointsOptionsParams opts
    )

-- | 'fleetSearch' runs a search against a single index that is only
-- executed once the supplied checkpoints are visible for search. Maps to
-- @POST /{index}/_fleet/_fleet_search@ with 'defaultFleetSearchOptions'
-- (no query parameters). The request body and response are the standard
-- 'Search' \/ 'SearchResult' shapes. The target index must resolve to a
-- single concrete index.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/fleet-search.html>.
fleetSearch ::
  (FromJSON a) =>
  IndexName ->
  Search ->
  BHRequest StatusDependant (SearchResult a)
fleetSearch indexName =
  fleetSearchWith indexName defaultFleetSearchOptions

-- | Like 'fleetSearch' but accepts 'FleetSearchOptions' carrying the
-- documented URI parameters @wait_for_checkpoints@ and
-- @allow_partial_search_results@. 'defaultFleetSearchOptions' makes this
-- byte-for-byte equivalent to 'fleetSearch'.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/fleet-search.html>.
fleetSearchWith ::
  (FromJSON a) =>
  IndexName ->
  FleetSearchOptions ->
  Search ->
  BHRequest StatusDependant (SearchResult a)
fleetSearchWith indexName opts search =
  post url' (encode search)
  where
    url' =
      [unIndexName indexName, "_fleet", "_fleet_search"]
        `withQueries` fleetSearchOptionsParams opts

-- | 'fleetMultiSearch' runs several fleet searches in a single request,
-- following the same NDJSON body shape as @_msearch@. Maps to
-- @POST /{index}/_fleet/_fleet_msearch@ with 'defaultFleetSearchOptions'.
-- Each sub-request is a 'MultiSearchItem' (header + 'Search'); the index
-- pinned by the URL applies to every sub-request.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/fleet-multi-search.html>.
fleetMultiSearch ::
  (FromJSON a) =>
  IndexName ->
  NonEmpty MultiSearchItem ->
  BHRequest StatusDependant (MultiSearchResponse a)
fleetMultiSearch indexName =
  fleetMultiSearchWith indexName defaultFleetSearchOptions

-- | Like 'fleetMultiSearch' but accepts 'FleetSearchOptions' carrying the
-- documented URI parameters @wait_for_checkpoints@ and
-- @allow_partial_search_results@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/fleet-multi-search.html>.
fleetMultiSearchWith ::
  (FromJSON a) =>
  IndexName ->
  FleetSearchOptions ->
  NonEmpty MultiSearchItem ->
  BHRequest StatusDependant (MultiSearchResponse a)
fleetMultiSearchWith indexName opts items =
  post url' (encodeMultiSearchItems items)
  where
    url' =
      [unIndexName indexName, "_fleet", "_fleet_msearch"]
        `withQueries` fleetSearchOptionsParams opts

-- Repositories metering API
--

-- * @GET /_nodes/_repositories_metering@ — 'getRepositoriesMetering'

--   (cluster-wide)

-- * @GET /_nodes/{selection}/_repositories_metering@ — scoped to a

--   'NodeSelection'

-- * @DELETE /_nodes/_repositories_metering@ — 'deleteRepositoriesMetering'

--   (resets the metering counters, returns 'Acknowledged')

-- * @DELETE /_nodes/{selection}/_repositories_metering@ — scoped

-- | 'getRepositoriesMetering' reports the repository metering counters
-- (snapshot creation\/deletion throughput and counts) for each
-- cloud-backed repository on each node. Maps to
-- @GET /_nodes/_repositories_metering@ when @Nothing@, or
-- @GET /_nodes/{seg}/_repositories_metering@ scoped to a
-- 'NodeSelection' when @'Just'@ (the segment rendered by
-- 'nodeSelectionSeg': @_local@, @_all@, or a comma-joined selector list).
--
-- * @'Nothing'@           → @GET /_nodes/_repositories_metering@
-- * @'Just' 'LocalNode'@  → @GET /_nodes/_local/_repositories_metering@
-- * @'Just' 'AllNodes'@   → @GET /_nodes/_all/_repositories_metering@
-- * @'Just' ('NodeList' ...)@ → @GET /_nodes/{csv}/_repositories_metering@
--
-- Repositories metering is part of the free (basic) X-Pack tier and is
-- available across ES 7.x\/8.x\/9.x with the same wire surface, so the
-- request lives in the Common layer and is re-exported by every ES
-- client module. OpenSearch does not implement this API.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/repository-metering-api.html>.
getRepositoriesMetering ::
  Maybe NodeSelection ->
  BHRequest StatusDependant RepositoriesMeteringResponse
getRepositoriesMetering mSelection =
  get @StatusDependant endpoint
  where
    endpoint = case mSelection of
      Nothing -> ["_nodes", "_repositories_metering"]
      Just sel -> ["_nodes", nodeSelectionSeg sel, "_repositories_metering"]

-- | 'deleteRepositoriesMetering' resets the repository metering counters
-- to zero across the cluster (@'Nothing'@) or on a node selection
-- (@'Just'@). Maps to @DELETE /_nodes/_repositories_metering@ or
-- @DELETE /_nodes/{seg}/_repositories_metering@ and returns
-- 'Acknowledged' on success.
--
-- * @'Nothing'@           → @DELETE /_nodes/_repositories_metering@
-- * @'Just' 'LocalNode'@  → @DELETE /_nodes/_local/_repositories_metering@
-- * @'Just' 'AllNodes'@   → @DELETE /_nodes/_all/_repositories_metering@
-- * @'Just' ('NodeList' ...)@ → @DELETE /_nodes/{csv}/_repositories_metering@
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/repository-metering-api.html>.
deleteRepositoriesMetering ::
  Maybe NodeSelection ->
  BHRequest StatusDependant Acknowledged
deleteRepositoriesMetering mSelection =
  delete endpoint
  where
    endpoint = case mSelection of
      Nothing -> ["_nodes", "_repositories_metering"]
      Just sel -> ["_nodes", nodeSelectionSeg sel, "_repositories_metering"]

------------------------------------------------------------------------------
-- Searchable snapshots API
--

-- * @POST /_snapshot/{repository}/{snapshot}/_mount@ — 'mountSearchableSnapshot'

-- * @GET /_searchable_snapshots/stats@ — 'getSearchableSnapshotsStats'

-- * @POST /_searchable_snapshots/{index}/_cache_stats@ — 'getSearchableSnapshotsCacheStats'

-- * @POST /_searchable_snapshots/{index}/_clear_cache@ — 'clearSearchableSnapshotsCache'

-- | 'mountSearchableSnapshot' mounts a snapshot as a searchable index.
-- Maps to @POST /_snapshot/{repository}/{snapshot}/_mount@ with the
-- default options (no query parameters). The 'SearchableSnapshotMount'
-- body names the mounted index (@index@) and may override
-- @renamed_index@, @index_settings@ and @ignore_index_settings@. The
-- response is a 'SearchableSnapshotMountResponse'; when
-- @wait_for_completion@ is @true@ (the default) it carries the full
-- snapshot-restore object under @snapshot@.
--
-- Use 'mountSearchableSnapshotWith' to set @master_timeout@,
-- @wait_for_completion@ or @storage@.
--
-- Searchable snapshots are part of the X-Pack tier and are available
-- across ES 7.x\/8.x\/9.x with the same wire surface, so the request
-- lives in the Common layer and is re-exported by every ES client
-- module. OpenSearch does not implement this API.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/searchable-snapshots-api.html>.
mountSearchableSnapshot ::
  SnapshotRepoName ->
  SnapshotName ->
  SearchableSnapshotMount ->
  BHRequest StatusDependant SearchableSnapshotMountResponse
mountSearchableSnapshot repo snap body =
  mountSearchableSnapshotWith repo snap body defaultSearchableSnapshotMountOptions

-- | Like 'mountSearchableSnapshot' but accepts 'SearchableSnapshotMountOptions'
-- carrying the documented URI parameters @master_timeout@,
-- @wait_for_completion@ and @storage@. 'defaultSearchableSnapshotMountOptions'
-- makes this byte-for-byte equivalent to 'mountSearchableSnapshot'.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/searchable-snapshots-api.html>.
mountSearchableSnapshotWith ::
  SnapshotRepoName ->
  SnapshotName ->
  SearchableSnapshotMount ->
  SearchableSnapshotMountOptions ->
  BHRequest StatusDependant SearchableSnapshotMountResponse
mountSearchableSnapshotWith repo snap body opts =
  post
    (["_snapshot", snapshotRepoName repo, snapshotName snap, "_mount"] `withQueries` searchableSnapshotMountOptionsParams opts)
    (encode body)

-- | 'getSearchableSnapshotsStats' reports cluster-wide and per-index
-- (and per-node) statistics for searchable snapshots. Maps to
-- @GET /_searchable_snapshots/stats@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/searchable-snapshots-api.html>.
getSearchableSnapshotsStats ::
  BHRequest StatusDependant SearchableSnapshotsStatsResponse
getSearchableSnapshotsStats =
  get ["_searchable_snapshots", "stats"]

-- | 'getSearchableSnapshotsCacheStats' reports cache statistics for a
-- single searchable-snapshot index. Maps to
-- @POST /_searchable_snapshots/{index}/_cache_stats@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/searchable-snapshots-api.html>.
getSearchableSnapshotsCacheStats ::
  IndexName ->
  BHRequest StatusDependant SearchableSnapshotsCacheStatsResponse
getSearchableSnapshotsCacheStats indexName =
  post ["_searchable_snapshots", unIndexName indexName, "_cache_stats"] emptyBody

-- | 'clearSearchableSnapshotsCache' evicts the node cache for a single
-- searchable-snapshot index. Maps to
-- @POST /_searchable_snapshots/{index}/_clear_cache@ and returns
-- 'Acknowledged' on success.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/searchable-snapshots-api.html>.
clearSearchableSnapshotsCache ::
  IndexName ->
  BHRequest StatusDependant Acknowledged
clearSearchableSnapshotsCache indexName =
  post ["_searchable_snapshots", unIndexName indexName, "_clear_cache"] emptyBody

------------------------------------------------------------------------------
-- Rollup API
--
-- The rollup endpoints live in the X-Pack tier and share the
-- ES 7.x\/8.x\/9.x wire surface, so the requests live in the Common layer
-- alongside SLM\/Enrich. Endpoint reference (see
-- "Database.Bloodhound.Internal.Versions.Common.Types.Rollup" for the
-- request/response shapes):

-- * @PUT /_rollup/job/{job_id}@ — 'putRollupJob'

-- * @GET /_rollup/job[/{job_id}|_all]@ — 'getRollupJob'

-- * @DELETE /_rollup/job/{job_id}@ — 'deleteRollupJob'

-- * @POST /_rollup/job/{job_id}/_start@ — 'startRollupJob'

-- * @POST /_rollup/job/{job_id}/_stop@ — 'stopRollupJob' / 'stopRollupJobWith'

-- * @GET /_rollup/job[/{job_id}|_all]/_stats@ — 'getRollupJobStats'

-- * @GET /_rollup/data/{index}@ — 'getRollupCapabilities'

-- * @GET /{target}/_rollup/data@ — 'getRollupIndexCapabilities'

-- * @GET /{index}/_rollup_search@ — 'rollupSearchByIndex'

-- | 'putRollupJob' creates or replaces a rollup job by id. Maps to
-- @PUT /_rollup/job/{job_id}@ and returns 'Acknowledged' on success. The
-- 'RollupJobConfig' body is encoded verbatim.
--
-- Rollup is part of the X-Pack tier and is available across ES
-- 7.x\/8.x\/9.x (deprecated in 8.x but still functional) with the same wire
-- surface, so the request lives in the Common layer and is re-exported by
-- every ES client module. OpenSearch does not implement the X-Pack rollup
-- API.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/rollup-put-job.html>.
putRollupJob ::
  RollupJobId ->
  RollupJobConfig ->
  BHRequest StatusDependant Acknowledged
putRollupJob (RollupJobId jobId) config =
  put ["_rollup", "job", jobId] (encode config)

-- | 'getRollupJob' retrieves the configuration, status, and stats of
-- rollup jobs.
--
-- * @'Nothing'@  → @GET /_rollup/job@ returns every job on the cluster.
-- * @'Just' jid@ → @GET /_rollup/job/{jid}@ returns just that one (as a
--   singleton list inside the 'GetRollupJobsResponse').
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/rollup-get-job.html>.
getRollupJob ::
  Maybe RollupJobId ->
  BHRequest StatusDependant GetRollupJobsResponse
getRollupJob mJobId =
  get @StatusDependant endpoint
  where
    endpoint = case mJobId of
      Nothing -> ["_rollup", "job"]
      Just (RollupJobId jobId) -> ["_rollup", "job", jobId]

-- | 'deleteRollupJob' deletes a single rollup job by id. Maps to
-- @DELETE /_rollup/job/{job_id}@ and returns 'Acknowledged' on success.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/rollup-delete-job.html>.
deleteRollupJob ::
  RollupJobId ->
  BHRequest StatusDependant Acknowledged
deleteRollupJob (RollupJobId jobId) =
  delete ["_rollup", "job", jobId]

-- | 'startRollupJob' starts an existing, stopped rollup job. Maps to
-- @POST /_rollup/job/{job_id}/_start@ and returns 'RollupJobStarted'.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/rollup-start-job.html>.
startRollupJob ::
  RollupJobId ->
  BHRequest StatusDependant RollupJobStarted
startRollupJob (RollupJobId jobId) =
  postNoBody ["_rollup", "job", jobId, "_start"]

-- | 'stopRollupJob' stops an existing, started rollup job asynchronously.
-- Maps to @POST /_rollup/job/{job_id}/_stop@ and returns 'RollupJobStopped'.
-- Use 'stopRollupJobWith' to block until the job has fully stopped
-- (@wait_for_completion=true@) or to override the @timeout@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/rollup-stop-job.html>.
stopRollupJob ::
  RollupJobId ->
  BHRequest StatusDependant RollupJobStopped
stopRollupJob jobId =
  stopRollupJobWith jobId defaultStopRollupJobOptions

-- | Like 'stopRollupJob' but accepts 'StopRollupJobOptions' carrying the
-- documented URI parameters @wait_for_completion@ and @timeout@.
-- 'defaultStopRollupJobOptions' makes this byte-for-byte equivalent to
-- 'stopRollupJob'.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/rollup-stop-job.html>.
stopRollupJobWith ::
  RollupJobId ->
  StopRollupJobOptions ->
  BHRequest StatusDependant RollupJobStopped
stopRollupJobWith (RollupJobId jobId) opts =
  postNoBody
    (["_rollup", "job", jobId, "_stop"] `withQueries` stopRollupJobOptionsParams opts)

-- | 'getRollupJobStats' retrieves the transient statistics (and current
-- state) of rollup jobs. Maps to @GET /_rollup/job[/{job_id}|_all]/_stats@.
--
-- * @'Nothing'@  → @GET /_rollup/job/_stats@ returns every job's stats.
-- * @'Just' jid@ → @GET /_rollup/job/{jid}/_stats@ returns just that one.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/rollup-apis.html>.
getRollupJobStats ::
  Maybe RollupJobId ->
  BHRequest StatusDependant GetRollupJobStatsResponse
getRollupJobStats mJobId =
  get @StatusDependant endpoint
  where
    endpoint = case mJobId of
      Nothing -> ["_rollup", "job", "_stats"]
      Just (RollupJobId jobId) -> ["_rollup", "job", jobId, "_stats"]

-- | 'getRollupCapabilities' returns the capabilities of any rollup jobs
-- configured for a given index or index pattern. Maps to
-- @GET /_rollup/data/{index}@. Use @_all@ to fetch capabilities from all
-- jobs.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/rollup-get-rollup-caps.html>.
getRollupCapabilities ::
  IndexPattern ->
  BHRequest StatusDependant RollupCapabilitiesResponse
getRollupCapabilities (IndexPattern indexPattern) =
  get ["_rollup", "data", indexPattern]

-- | 'getRollupIndexCapabilities' returns the rollup capabilities of all
-- jobs stored inside a rollup index (or index pattern). Maps to
-- @GET /{target}/_rollup/data@.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/rollup-get-rollup-index-caps.html>.
getRollupIndexCapabilities ::
  IndexName ->
  BHRequest StatusDependant RollupCapabilitiesResponse
getRollupIndexCapabilities indexName =
  get [unIndexName indexName, "_rollup", "data"]

-- | 'rollupSearchByIndex' runs a standard 'Search' against rolled-up data.
-- Maps to @GET /{index}/_rollup_search@ with the search body carried in
-- the request body (the method is GET per the ES docs; 'getWithBody' is
-- used so the body survives proxies that strip it from a body-less GET).
-- The response shape is the standard 'SearchResult'; hits are always
-- empty (@size@ must be @0@ or omitted), so callers typically inspect
-- 'aggregations' instead.
--
-- At least one index (or pattern) must be specified; omitting the target
-- or using @_all@ is not permitted by the rollup search endpoint.
--
-- See
-- <https://www.elastic.co/guide/en/elasticsearch/reference/7.17/rollup-search.html>.
rollupSearchByIndex ::
  (FromJSON a) =>
  IndexName ->
  Search ->
  BHRequest StatusDependant (SearchResult a)
rollupSearchByIndex indexName =
  rollupSearchByIndexWith indexName defaultSearchOptions

-- | Like 'rollupSearchByIndex' but accepts a 'SearchOptions' record for
-- URI-level parameters such as @preference@ and @routing@.
rollupSearchByIndexWith ::
  (FromJSON a) =>
  IndexName ->
  SearchOptions ->
  Search ->
  BHRequest StatusDependant (SearchResult a)
rollupSearchByIndexWith indexName opts search =
  getWithBody
    ([unIndexName indexName, "_rollup_search"] `withQueries` searchOptionsParams opts (searchType search))
    (encode search)