{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications    #-}

module Antiope.S3.Lazy
  ( unsafeDownload
  , unsafeDownloadRequest
  , unsafeDownloadIfModifiedSince
  , download
  , downloadRequest
  , downloadFromS3Uri
  , downloadIfModifiedSince
  , listObjectsV2
  , listS3Uris
  , dlistObjectsV2
  , dlistS3Uris
  , s3UriToListObjectsV2
  , DownloadResult (..)
  , S3Uri(..)
  , AWS.BucketName
  , AWS.ObjectKey
  , UTCTime
  ) where

import Antiope.Core.Error
import Antiope.S3.GetObject
import Antiope.S3.Types             (DownloadResult (..), S3Uri (S3Uri), s3UriToListObjectsV2)
import Control.Lens
import Control.Monad.IO.Unlift
import Control.Monad.Trans.Resource
import Data.Maybe                   (fromMaybe)
import Data.Time.Clock              (UTCTime)
import Data.Time.Clock.POSIX        (posixSecondsToUTCTime)
import Network.AWS                  (MonadAWS)
import Network.HTTP.Types.Status    (Status (..))

import qualified Antiope.S3.Internal  as I
import qualified Data.ByteString.Lazy as LBS
import qualified Data.DList           as DL
import qualified Network.AWS          as AWS
import qualified Network.AWS.S3       as AWS
import qualified System.IO.Unsafe     as IO

unsafeDownloadRequest :: (MonadAWS m, MonadResource m)
  => AWS.GetObject
  -> m LBS.ByteString
unsafeDownloadRequest req = do
  resp <- AWS.send req
  lazyByteString (resp ^. AWS.gorsBody)

unsafeDownload :: (MonadAWS m, MonadResource m)
  => AWS.BucketName
  -> AWS.ObjectKey
  -> m LBS.ByteString
unsafeDownload bucketName objectKey = unsafeDownloadRequest (AWS.getObject bucketName objectKey)

unsafeDownloadIfModifiedSince :: (MonadAWS m, MonadResource m)
  => AWS.BucketName
  -> AWS.ObjectKey
  -> Maybe UTCTime
  -> m (UTCTime, LBS.ByteString)
unsafeDownloadIfModifiedSince bkt obj since = do
  let req = AWS.getObject bkt obj & AWS.goIfModifiedSince .~ since
  resp <- AWS.send req
  -- in practice AWS will never return Nothing for the timestamp, but the API allows it
  let modified = fromMaybe (posixSecondsToUTCTime 0) (resp ^. AWS.gorsLastModified)
  body <- lazyByteString (resp ^. AWS.gorsBody)
  pure (modified, body)

downloadIfModifiedSince :: (MonadAWS m, MonadResource m)
  => S3Uri
  -> Maybe UTCTime
  -> m (DownloadResult LBS.ByteString)
downloadIfModifiedSince uri@(S3Uri bucketName objectKey) since =
  handleServiceError
    (unsafeDownloadIfModifiedSince bucketName objectKey since)
    (\(ts, bs) -> Downloaded ts uri bs)
    (\case
        Status 404 _ -> Just $ NotFound uri
        Status 304 _ -> Just $ NotModified uri
        _            -> Nothing
    )

downloadRequest :: (MonadAWS m, MonadResource m)
  => AWS.GetObject
  -> m (Maybe LBS.ByteString)
downloadRequest req = handle404ToNone (unsafeDownloadRequest req)

download :: (MonadAWS m, MonadResource m)
  => AWS.BucketName
  -> AWS.ObjectKey
  -> m (Maybe LBS.ByteString)
download bucketName objectKey = downloadRequest (AWS.getObject bucketName objectKey)

downloadFromS3Uri :: (MonadAWS m, MonadResource m)
  => S3Uri
  -> m (Maybe LBS.ByteString)
downloadFromS3Uri (S3Uri b k) = download b k

dlistObjectsV2 :: (MonadAWS m, MonadResource m, MonadUnliftIO m)
  => AWS.ListObjectsV2
  -> m (DL.DList AWS.ListObjectsV2Response)
dlistObjectsV2 req = do
  f <- askUnliftIO
  r <- AWS.send req
  case r ^. AWS.lovrsIsTruncated of
    Just True -> do
      rs <- liftIO $ IO.unsafeInterleaveIO (unliftIO f (dlistObjectsV2 (I.nextPageReq req r)))
      return (DL.cons r rs)
    _ -> return (DL.singleton r)

dlistS3Uris :: (MonadAWS m, MonadResource m, MonadUnliftIO m)
  => AWS.ListObjectsV2
  -> m (DL.DList S3Uri)
dlistS3Uris req = dlistObjectsV2 req >>= toS3Uris
  where toS3Uris responses  = return (responses >>= toS3Uris')
        toS3Uris' response  = do
          c <- response ^. AWS.lovrsContents & DL.fromList
          response ^.. AWS.lovrsName . _Just & DL.fromList <&> \bucketName -> S3Uri bucketName (c ^. AWS.oKey)

listObjectsV2 :: (MonadAWS m, MonadResource m, MonadUnliftIO m)
  => AWS.ListObjectsV2
  -> m [AWS.ListObjectsV2Response]
listObjectsV2 req = DL.toList <$> dlistObjectsV2 req

listS3Uris :: (MonadAWS m, MonadResource m, MonadUnliftIO m)
  => AWS.ListObjectsV2
  -> m [S3Uri]
listS3Uris req = DL.toList <$> dlistS3Uris req