{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UndecidableInstances #-}
module OpenAPI.Common
  ( Configuration (..),
    doCallWithConfiguration,
    doCallWithConfigurationM,
    doBodyCallWithConfiguration,
    doBodyCallWithConfigurationM,
    runWithConfiguration,
    MonadHTTP (..),
    stringifyModel,
    StringifyModel,
    SecurityScheme (..),
    AnonymousSecurityScheme (..),
    textToByte,
    JsonByteString (..),
    JsonDateTime (..),
    RequestBodyEncoding (..),
  )
where
import qualified Control.Exception as Exception
import qualified Control.Monad.Reader as MR
import qualified Control.Monad.Trans.Class as MT
import qualified Data.Aeson as Aeson
import qualified Data.Bifunctor as BF
import qualified Data.ByteString.Char8 as B8
import qualified Data.HashMap.Strict as HMap
import qualified Data.Maybe as Maybe
import qualified Data.Scientific as Scientific
import qualified Data.Text as T
import Data.Text (Text)
import qualified Data.Time.LocalTime as Time
import qualified Data.Vector as Vector
import GHC.Generics
import qualified Network.HTTP.Client as HC
import qualified Network.HTTP.Simple as HS
class Monad m => MonadHTTP m where
  httpBS :: HS.Request -> m (Either HS.HttpException (HS.Response B8.ByteString))
instance MonadHTTP IO where
  httpBS request =
    BF.first (\e -> e :: HS.HttpException)
      <$> Exception.try (HS.httpBS request)
instance MonadHTTP m => MonadHTTP (MR.ReaderT r m) where
  httpBS = MT.lift . httpBS
data Configuration s
  = Configuration
      { configBaseURL :: Text,
        configSecurityScheme :: s
      }
  deriving (Show, Ord, Eq, Generic)
data RequestBodyEncoding
  = 
    RequestBodyEncodingJSON
  | 
    RequestBodyEncodingFormData
class SecurityScheme s where
  authenticateRequest :: s -> HS.Request -> HS.Request
data AnonymousSecurityScheme = AnonymousSecurityScheme
instance SecurityScheme AnonymousSecurityScheme where
  authenticateRequest = const id
runWithConfiguration :: SecurityScheme s => Configuration s -> MR.ReaderT (Configuration s) m a -> m a
runWithConfiguration = flip MR.runReaderT
doCallWithConfiguration ::
  (MonadHTTP m, SecurityScheme s) =>
  
  Configuration s ->
  
  Text ->
  
  Text ->
  
  [(Text, Maybe String)] ->
  
  m (Either HS.HttpException (HS.Response B8.ByteString))
doCallWithConfiguration config method path queryParams =
  httpBS $ createBaseRequest config method path queryParams
doCallWithConfigurationM ::
  (MonadHTTP m, SecurityScheme s) =>
  Text ->
  Text ->
  [(Text, Maybe String)] ->
  MR.ReaderT (Configuration s) m (Either HS.HttpException (HS.Response B8.ByteString))
doCallWithConfigurationM method path queryParams = do
  config <- MR.ask
  MT.lift $ doCallWithConfiguration config method path queryParams
doBodyCallWithConfiguration ::
  (MonadHTTP m, SecurityScheme s, Aeson.ToJSON body) =>
  
  Configuration s ->
  
  Text ->
  
  Text ->
  
  [(Text, Maybe String)] ->
  
  Maybe body ->
  
  RequestBodyEncoding ->
  
  m (Either HS.HttpException (HS.Response B8.ByteString))
doBodyCallWithConfiguration config method path queryParams Nothing _ = doCallWithConfiguration config method path queryParams
doBodyCallWithConfiguration config method path queryParams (Just body) RequestBodyEncodingJSON =
  httpBS $ HS.setRequestMethod (textToByte method) $ HS.setRequestBodyJSON body baseRequest
  where
    baseRequest = createBaseRequest config method path queryParams
doBodyCallWithConfiguration config method path queryParams (Just body) RequestBodyEncodingFormData =
  httpBS $ HS.setRequestMethod (textToByte method) $ HS.setRequestBodyURLEncoded byteStringData baseRequest
  where
    baseRequest = createBaseRequest config method path queryParams
    byteStringData = createFormData body
doBodyCallWithConfigurationM ::
  (MonadHTTP m, SecurityScheme s, Aeson.ToJSON body) =>
  Text ->
  Text ->
  [(Text, Maybe String)] ->
  Maybe body ->
  RequestBodyEncoding ->
  MR.ReaderT (Configuration s) m (Either HS.HttpException (HS.Response B8.ByteString))
doBodyCallWithConfigurationM method path queryParams body encoding = do
  config <- MR.ask
  MT.lift $ doBodyCallWithConfiguration config method path queryParams body encoding
createBaseRequest ::
  SecurityScheme s =>
  
  Configuration s ->
  
  Text ->
  
  Text ->
  
  [(Text, Maybe String)] ->
  
  HS.Request
createBaseRequest config method path queryParams =
  authenticateRequest (configSecurityScheme config)
    $ HS.setRequestMethod (textToByte method)
    $ HS.setRequestQueryString query
    $ HS.setRequestPath
      (B8.pack (T.unpack $ byteToText basePathModifier <> path))
      baseRequest
  where
    baseRequest = parseURL $ configBaseURL config
    basePath = HC.path baseRequest
    basePathModifier =
      if basePath == B8.pack "/" && T.isPrefixOf "/" path
        then ""
        else basePath
    
    query = [(textToByte a, Just $ B8.pack b) | (a, Just b) <- queryParams]
createFormData :: (Aeson.ToJSON a) => a -> [(B8.ByteString, B8.ByteString)]
createFormData body =
  let formData = jsonToFormData $ Aeson.toJSON body
   in fmap (BF.bimap textToByte textToByte) formData
byteToText :: B8.ByteString -> Text
byteToText = T.pack . B8.unpack
textToByte :: Text -> B8.ByteString
textToByte = B8.pack . T.unpack
parseURL :: Text -> HS.Request
parseURL url =
  Maybe.fromMaybe HS.defaultRequest
    $ HS.parseRequest
    $ T.unpack url
jsonToFormData :: Aeson.Value -> [(Text, Text)]
jsonToFormData = jsonToFormDataPrefixed ""
jsonToFormDataPrefixed :: Text -> Aeson.Value -> [(Text, Text)]
jsonToFormDataPrefixed prefix (Aeson.Number a) = case Scientific.toBoundedInteger a :: Maybe Int of
  Just myInt -> [(prefix, T.pack $ show myInt)]
  Nothing -> [(prefix, T.pack $ show a)]
jsonToFormDataPrefixed prefix (Aeson.Bool True) = [(prefix, T.pack "true")]
jsonToFormDataPrefixed prefix (Aeson.Bool False) = [(prefix, T.pack "false")]
jsonToFormDataPrefixed _ Aeson.Null = []
jsonToFormDataPrefixed prefix (Aeson.String a) = [(prefix, a)]
jsonToFormDataPrefixed "" (Aeson.Object object) =
  HMap.toList object >>= uncurry jsonToFormDataPrefixed
jsonToFormDataPrefixed prefix (Aeson.Object object) =
  HMap.toList object >>= (\(x, y) -> jsonToFormDataPrefixed (prefix <> "[" <> x <> "]") y)
jsonToFormDataPrefixed prefix (Aeson.Array vector) =
  Vector.toList vector >>= jsonToFormDataPrefixed (prefix <> "[]")
class Show a => StringifyModel a where
  
  
  
  
  
  
  
  stringifyModel :: a -> String
instance StringifyModel String where
  
  stringifyModel = id
instance {-# OVERLAPS #-} Show a => StringifyModel a where
  
  stringifyModel = show
newtype JsonByteString = JsonByteString B8.ByteString
  deriving (Show, Eq, Ord)
instance Aeson.ToJSON JsonByteString where
  toJSON (JsonByteString s) = Aeson.toJSON $ B8.unpack s
instance Aeson.FromJSON JsonByteString where
  parseJSON (Aeson.String s) = pure $ JsonByteString $ textToByte s
  parseJSON _ = fail "Value cannot be converted to a 'JsonByteString'"
newtype JsonDateTime = JsonDateTime Time.ZonedTime
  deriving (Show)
instance Eq JsonDateTime where
  (JsonDateTime d1) == (JsonDateTime d2) = Time.zonedTimeToUTC d1 == Time.zonedTimeToUTC d2
instance Ord JsonDateTime where
  (JsonDateTime d1) <= (JsonDateTime d2) = Time.zonedTimeToUTC d1 <= Time.zonedTimeToUTC d2
instance Aeson.ToJSON JsonDateTime where
  toJSON (JsonDateTime d) = Aeson.toJSON d
instance Aeson.FromJSON JsonDateTime where
  parseJSON o = JsonDateTime <$> Aeson.parseJSON o