{-# LANGUAGE ViewPatterns      #-}

{-| Edit 'sensitiveKeywords' to extend the list of redacted fields. -}
module Gargantext.API.Middleware (
  logStdoutDevSanitised
  ) where

import Control.Lens
import Control.Monad.Logger
import Data.Aeson qualified as A
import Data.Aeson.Lens qualified as L
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Builder qualified as BS
import Data.ByteString.Char8 qualified as C8
import Data.ByteString.Lazy qualified as B
import Data.CaseInsensitive qualified as CI
import Data.List qualified as L
import Data.String
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Network.HTTP.Types
import Network.HTTP.Types.Header
import Network.Wai
import Network.Wai.Middleware.RequestLogger
import Prelude
import System.Console.ANSI

-- | Like 'logStdoutDev' from \"wai-extra\", but redacts (or omits altogether) payloads which might have
-- sensitive information
logStdoutDevSanitised :: IO Middleware
logStdoutDevSanitised = mkRequestLogger $ defaultRequestLoggerSettings { outputFormat = CustomOutputFormatWithDetailsAndHeaders customOutput }
-- |
-- Like 'key', but uses 'at' instead of 'ix'. This is handy when
-- adding and removing object keys:
--
-- >>> "{\"a\": 100, \"b\": 200}" & atKey "a" .~ Nothing
-- "{\"b\":200}"
--
-- >>> "{\"a\": 100, \"b\": 200}" & atKey "c" ?~ String "300"
-- "{\"a\":100,\"b\":200,\"c\":\"300\"}"
atKey :: L.AsValue t => T.Text -> Traversal' t (Maybe A.Value)
atKey i = L._Object . at (fromString $ T.unpack i)
{-# INLINE atKey #-}

customOutput :: OutputFormatterWithDetailsAndHeaders
customOutput _zonedDate rq status _mb_response_size request_dur (sanitiseBody . mconcat -> reqbody) raw_response (map sanitiseHeader -> headers) =
  let params = map sanitiseQueryItem (queryString rq)
  in mkRequestLog params reqbody <> mkResponseLog

  where

    mkRequestLog :: [QueryItem] -> ByteString -> LogStr
    mkRequestLog params bdy =
        foldMap toLogStr (ansiMethod' (requestMethod rq))
            <> " "
            <> toLogStr (rawPathInfo rq)
            <> "\n"
            <> foldMap (\(k, mb_v) -> toLogStr $ show (k, mb_v)) params
            <> toLogStr bdy
            <> "\n"
            <> foldMap (\(k, v) -> toLogStr $ mconcat $ ansiColor' White $ "  " <> CI.original k <> ": " <> v <> "\n") headers
            <> "\n"

    mkResponseLog :: LogStr
    mkResponseLog =
      foldMap toLogStr (ansiColor' White "  Status: ")
          <> foldMap toLogStr (ansiStatusCode' status (C8.pack (show $ statusCode status) <> " " <> statusMessage status))
          <> " "
          <> (toLogStr . B.toStrict $ (BS.toLazyByteString raw_response))
          <> " "
          <> "Served in " <> toLogStr (C8.pack $ show $ request_dur)
          <> "\n"

sanitiseBody :: ByteString -> ByteString
sanitiseBody blob = L.foldr (\k acc -> over (atKey k) (updateField k) acc) blob sensitiveKeywords
  where
    updateField :: T.Text -> Maybe A.Value -> Maybe A.Value
    updateField _ Nothing = Nothing
    updateField k (Just x)
      | A.String _v <- x
      , k `elem` sensitiveKeywords
      = Just $ A.String "*****"
      | otherwise
      = Just x

sanitiseQueryItem :: QueryItem -> QueryItem
sanitiseQueryItem (k, mb_v)
  | TE.decodeUtf8 k `elem` sensitiveKeywords
  = (k, (\v -> if C8.null v then mempty else "*****") <$> mb_v)
  | otherwise
  = (k, mb_v)

-- /NOTE:/ Extend this list to filter for more sensitive keywords.
sensitiveKeywords :: [T.Text]
sensitiveKeywords = [
    "password"
  , "api_key"
  , "apiKey"
  , "pubmedAPIKey"
  ]


sanitiseHeader :: Header -> Header
sanitiseHeader (hName, content)
  | hName == hAuthorization = (hName, "*****")
  | hName == hCookie        = (hName, "*****")
  | hName == hSetCookie     = (hName, "*****")
  | otherwise               = (hName, content)

ansiColor' :: Color -> BS.ByteString -> [BS.ByteString]
ansiColor' color bs =
    [ C8.pack $ setSGRCode [SetColor Foreground Dull color]
    , bs
    , C8.pack $ setSGRCode [Reset]
    ]

-- | Tags http method with a unique color.
ansiMethod' :: BS.ByteString -> [BS.ByteString]
ansiMethod' m = case m of
    "GET" -> ansiColor' Cyan m
    "HEAD" -> ansiColor' Cyan m
    "PUT" -> ansiColor' Green m
    "POST" -> ansiColor' Yellow m
    "DELETE" -> ansiColor' Red m
    _ -> ansiColor' Magenta m

ansiStatusCode' :: Status -> ByteString -> [BS.ByteString]
ansiStatusCode' (Status c _) t = case C8.take 1 (C8.pack . show $ c) of
    "2" -> ansiColor' Green t
    "3" -> ansiColor' Yellow t
    "4" -> ansiColor' Red t
    "5" -> ansiColor' Magenta t
    _ -> ansiColor' Blue t
