module Foundation.Yesod.StaticContent
  ( addStaticContent
  ) where

import Import.NoFoundation hiding (addStaticContent)

import Foundation.Type

import qualified Database.Memcached.Binary.IO as Memcached

import qualified Data.ByteString.Lazy as Lazy
import qualified Data.ByteString.Base64.URL as Base64 (encodeUnpadded)
import Data.ByteArray (convert)
import Crypto.Hash (SHAKE256)
import Crypto.Hash.Conduit (sinkHash)
import Data.Bits (Bits(zeroBits))

import qualified Data.Conduit.Combinators as C


addStaticContent :: Text
                 -> Text
                 -> Lazy.ByteString
                 -> HandlerFor UniWorX (Maybe (Either Text (Route UniWorX, [(Text, Text)])))
addStaticContent ext _mime content = do
  UniWorX{appWidgetMemcached, appSettings'} <- getYesod
  for ((,) <$> appWidgetMemcached <*> appWidgetMemcachedConf appSettings') $ \(mConn, WidgetMemcachedConf{ widgetMemcachedConf = MemcachedConf { memcachedExpiry }, widgetMemcachedBaseUrl }) -> do
    let expiry = maybe 0 ceiling memcachedExpiry
        touch = liftIO $ Memcached.touch expiry (encodeUtf8 $ pack fileName) mConn
        add = liftIO $ Memcached.add zeroBits expiry (encodeUtf8 $ pack fileName) content mConn
        absoluteLink = unpack widgetMemcachedBaseUrl </> fileName
    catchIf Memcached.isKeyNotFound touch . const $
      handleIf Memcached.isKeyExists (const $ return ()) add
    return . Left $ pack absoluteLink
  where
    -- Generate a unique filename based on the content itself, this is used
    -- for deduplication so a collision resistant hash function is required
    --
    -- SHA-3 (SHAKE256) seemed to be a future-proof choice
    --
    -- Length of hash is 144 bits ~~instead of MD5's 128, so as to avoid
    -- padding after base64-conversion~~ for backwards compatability
    fileName = (<.> unpack ext)
             . unpack
             . decodeUtf8
             . Base64.encodeUnpadded
             . (convert :: Digest (SHAKE256 144) -> ByteString)
             . runConduitPure
             $ C.sourceLazy content .| sinkHash
