{-# LANGUAGE NamedFieldPuns #-}
{-|
Module      : PostgREST.Query.Statements
Description : PostgREST main queries
-}
module PostgREST.Query.Statements
  ( mainWrite
  , mainRead
  , mainCall
  , postExplain
  ) where

import qualified Hasql.DynamicStatements.Snippet as SQL

import PostgREST.ApiRequest.Preferences
import PostgREST.MediaType              (MTVndPlanFormat (..),
                                         MediaType (..))
import PostgREST.Plan.CallPlan
import PostgREST.Plan.MutatePlan        as MTPlan
import PostgREST.Plan.ReadPlan
import PostgREST.Query.QueryBuilder
import PostgREST.Query.SqlFragment
import PostgREST.RangeQuery             (NonnegRange)
import PostgREST.SchemaCache.Routine    (MediaHandler (..), Routine)

import Protolude

mainWrite :: ReadPlanTree -> MutatePlan -> MediaType -> MediaHandler ->
             Maybe PreferRepresentation -> Maybe PreferResolution -> SQL.Snippet
mainWrite rPlan mtplan mt handler rep resolution = mtSnippet mt snippet
 where
  checkUpsert snip = if isInsert && (isPut || resolution == Just MergeDuplicates) then snip else "''"
  pgrstInsertedF = checkUpsert "nullif(current_setting('pgrst.inserted', true),'')::int"
  snippet =
    "WITH " <> sourceCTE <> " AS (" <> mutateQuery <> ") " <>
    "SELECT " <>
      "'' AS total_result_set, " <>
      "pg_catalog.count(_postgrest_t) AS page_total, " <>
      locF <> " AS header, " <>
      handlerF Nothing handler <> " AS body, " <>
      responseHeadersF <> " AS response_headers, " <>
      responseStatusF  <> " AS response_status, " <>
      pgrstInsertedF <> " AS response_inserted " <>
    "FROM (" <> selectF <> ") _postgrest_t"

  locF =
    if isInsert && rep == Just HeadersOnly
      then
        "CASE WHEN pg_catalog.count(_postgrest_t) = 1 " <>
          "THEN coalesce(" <> locationF pkCols <> ", " <> noLocationF <> ") " <>
          "ELSE " <> noLocationF <> " " <>
        "END"
      else noLocationF

  selectF
    -- prevent using any of the column names in ?select= when no response is returned from the CTE
    | handler == NoAgg = "SELECT * FROM " <> sourceCTE
    | otherwise        = selectQuery

  selectQuery = readPlanToQuery rPlan
  mutateQuery = mutatePlanToQuery mtplan
  (isPut, isInsert, pkCols) = case mtplan of
    MTPlan.Insert{MTPlan.where_,insPkCols} -> ((not . null) where_, True, insPkCols)
    _ -> (False,False, mempty);

mainRead :: ReadPlanTree -> SQL.Snippet -> Maybe PreferCount -> Maybe Integer ->
            NonnegRange -> MediaType -> MediaHandler -> SQL.Snippet
mainRead rPlan countQuery pCount maxRows range mt handler = mtSnippet mt snippet
 where
  snippet =
    "WITH " <> sourceCTE <> " AS ( " <> selectQuery <> " ) " <>
    countCTEF <> " " <>
    "SELECT " <>
      countResultF <> " AS total_result_set, " <>
      pageCountSelect <> " AS page_total, " <>
      handlerF Nothing handler <> " AS body, " <>
      responseHeadersF <> " AS response_headers, " <>
      responseStatusF <> " AS response_status, " <>
      "''" <> " AS response_inserted " <>
    "FROM ( SELECT * FROM " <> sourceCTE <> " ) _postgrest_t"

  (countCTEF, countResultF) = countF countQ pageCountSelect (shouldCount pCount) maxRows range
  selectQuery = readPlanToQuery rPlan
  pageCountSelect = pageCountSelectF Nothing
  countQ =
    if pCount == Just EstimatedCount then
      -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
      limitedQuery countQuery ((+ 1) <$> maxRows)
    else
      countQuery

mainCall :: Routine -> CallPlan -> ReadPlanTree -> Maybe PreferCount -> Maybe Integer ->
            NonnegRange-> MediaType -> MediaHandler -> SQL.Snippet
mainCall rout cPlan rPlan pCount maxRows range mt handler = mtSnippet mt snippet
  where
    snippet =
      "WITH " <> sourceCTE <> " AS (" <> callProcQuery <> ") " <>
      countCTEF <>
      "SELECT " <>
        countResultF <> " AS total_result_set, " <>
        pageCountSelect <> " AS page_total, " <>
        handlerF (Just rout) handler <> " AS body, " <>
        responseHeadersF <> " AS response_headers, " <>
        responseStatusF <> " AS response_status, " <>
        "''" <> " AS response_inserted " <>
      "FROM (" <> selectQuery <> ") _postgrest_t"

    (countCTEF, countResultF) = countF countQuery pageCountSelect (shouldCount pCount) maxRows range
    selectQuery = readPlanToQuery rPlan
    callProcQuery = callPlanToQuery cPlan
    countQuery = readPlanToCountQuery rPlan
    pageCountSelect = pageCountSelectF (Just rout)

-- This occurs after the main query runs, that's why it's prefixed with "post"
postExplain :: SQL.Snippet -> SQL.Snippet
postExplain = explainF PlanJSON mempty

mtSnippet :: MediaType -> SQL.Snippet -> SQL.Snippet
mtSnippet mediaType snippet = case mediaType of
  MTVndPlan _ fmt opts -> explainF fmt opts snippet
  _                    -> snippet
