module Stackctl.AWS.Core
  ( MonadAWS
  , send
  , paginate
  , await
  , withAuth
  , localEnv

    -- * "Control.Monad.AWS" extensions
  , simple
  , discover
  , assumeRole

    -- * Error-handling
  , handlingServiceError
  , formatServiceError

    -- * "Amazonka" extensions
  , AccountId (..)

    -- * "Amazonka" re-exports
  , Region (..)
  , FromText (..)
  , ToText (..)
  ) where

import Stackctl.Prelude

import Amazonka
  ( AWSRequest
  , AWSResponse
  , Region
  , ServiceError
  , serviceError_code
  , serviceError_message
  , serviceError_requestId
  , _Sensitive
  , _ServiceError
  )
import qualified Amazonka
import Amazonka.Auth.Keys (fromSession)
import Amazonka.Data.Text (FromText (..), ToText (..))
import qualified Amazonka.Env as Amazonka
import Amazonka.STS.AssumeRole
import Control.Monad.AWS
import Control.Monad.Logger (defaultLoc, toLogStr)
import Data.Typeable (typeRep)
import Stackctl.AWS.Orphans ()
import UnliftIO.Exception.Lens (handling)

discover :: MonadLoggerIO m => m Amazonka.Env
discover = do
  env <- liftIO $ Amazonka.newEnv Amazonka.discover
  loggerIO <- askLoggerIO

  let logger level = do
        loggerIO
          defaultLoc
          "Amazonka"
          ( case level of
              Amazonka.Info -> LevelInfo
              Amazonka.Error -> LevelError
              Amazonka.Debug -> LevelDebug
              Amazonka.Trace -> LevelOther "trace"
          )
          . toLogStr
  pure $ env & Amazonka.env_logger .~ logger

simple
  :: forall a m b
   . ( HasCallStack
     , MonadIO m
     , MonadAWS m
     , AWSRequest a
     , Typeable a
     , Typeable (AWSResponse a)
     )
  => a
  -> (AWSResponse a -> Maybe b)
  -> m b
simple req post = do
  resp <- send req

  let
    name = show $ typeRep $ Proxy @a
    err = name <> " successful, but processing the response failed"

  maybe (throwString err) pure $ post resp

assumeRole
  :: (MonadIO m, MonadAWS m)
  => Text
  -- ^ Role ARN
  -> Text
  -- ^ Session name
  -> m a
  -- ^ Action to run as the assumed role
  -> m a
assumeRole role sessionName f = do
  let req = newAssumeRole role sessionName

  assumeEnv <- simple req $ \resp -> do
    let creds = resp ^. assumeRoleResponse_credentials
    token <- creds ^. Amazonka.authEnv_sessionToken

    let
      accessKeyId = creds ^. Amazonka.authEnv_accessKeyId
      secretAccessKey = creds ^. Amazonka.authEnv_secretAccessKey . _Sensitive
      sessionToken = token ^. _Sensitive

    pure $ fromSession accessKeyId secretAccessKey sessionToken

  localEnv assumeEnv f

newtype AccountId = AccountId
  { unAccountId :: Text
  }
  deriving newtype (Eq, Ord, Show, ToJSON)

-- | Handle 'ServiceError', log it and 'exitFailure'
--
-- This is useful at the top-level of the app, where we'd be crashing anyway. It
-- makes things more readable and easier to debug.
handlingServiceError :: (MonadUnliftIO m, MonadLogger m) => m a -> m a
handlingServiceError =
  handling _ServiceError $ \e -> do
    logError
      $ "Exiting due to AWS Service error"
      :# [ "code" .= toText (e ^. serviceError_code)
         , "message" .= fmap toText (e ^. serviceError_message)
         , "requestId" .= fmap toText (e ^. serviceError_requestId)
         ]
    exitFailure

formatServiceError :: ServiceError -> Text
formatServiceError e =
  mconcat
    [ toText $ e ^. serviceError_code
    , maybe "" ((": " <>) . toText) $ e ^. serviceError_message
    , maybe "" (("\nRequest Id: " <>) . toText) $ e ^. serviceError_requestId
    ]
