module Utils.Cookies.Registered
  ( RegisteredCookie(..)
  , lookupRegisteredCookie, lookupRegisteredCookies
  , lookupRegisteredCookieJson, lookupRegisteredCookiesJson
  , setRegisteredCookie, setRegisteredCookie'
  , setRegisteredCookieJson, setRegisteredCookieJson'
  , modifyRegisteredCookieJson, modifyRegisteredCookieJson'
  , tellRegisteredCookieJson, tellRegisteredCookieJson'
  , deleteRegisteredCookie, deleteRegisteredCookie'
  ) where

import ClassyPrelude.Yesod

import Settings.Cookies

import Utils.Cookies
import Utils.PathPiece

import Data.Universe
import Control.Lens

import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Text as Aeson
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import qualified Data.ByteString.Base64.URL as Base64

import Web.Cookie (SetCookie(..))

import Data.Char (isAscii)
import Data.Monoid (Last(..))


data RegisteredCookie = CookieSession | CookieXSRFToken | CookieLang | CookieSystemMessageState | CookieActiveAuthTags
  deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
  deriving anyclass (Universe, Finite, Hashable)

nullaryPathPiece ''RegisteredCookie $ toUpper . camelToPathPiece' 1
pathPieceJSON ''RegisteredCookie
pathPieceJSONKey ''RegisteredCookie


_CookieEncoded :: Prism' Text Text
_CookieEncoded = prism' cEncode cDecode
  where
    b64Prefix = "base64url:"

    cDecode t
      | Just encoded <- Text.stripPrefix b64Prefix t
      = either (const Nothing) Just . Text.decodeUtf8' <=< either (const Nothing) Just . Base64.decode $ Text.encodeUtf8 encoded
      | Text.all isAscii t = Just t
      | otherwise = Nothing

    cEncode t
      | Text.all isAscii t
      , not $ b64Prefix `Text.isPrefixOf` t
      = t
      | otherwise
      = b64Prefix <> Text.decodeUtf8 (Base64.encode $ Text.encodeUtf8 t)

newtype RegisteredCookieCurrentValue = RegisteredCookieCurrentValue { getRegisteredCookieCurrentValue :: Maybe Text }
  deriving (Eq, Ord, Read, Show, Generic, Typeable)


-- Primitive
setRegisteredCookie' :: (Textual t, MonadHandler m, Yesod (HandlerSite m), HasCookieSettings RegisteredCookie (HandlerSite m)) => (SetCookie -> SetCookie) -> RegisteredCookie -> t -> m ()
setRegisteredCookie' modSet ident@(toPathPiece -> name) (review _CookieEncoded . repack -> content) = do
  path <- getCookiePath
  defSetCookie <- cookieSettingsToSetCookie . ($ ident) =<< getsYesod getCookieSettings

  setCookie $ modSet defSetCookie
    { setCookieName     = Text.encodeUtf8 name
    , setCookieValue    = Text.encodeUtf8 content
    , setCookiePath     = Just path
    }

  cacheBySet (Text.encodeUtf8 name) . RegisteredCookieCurrentValue $ Just content

setRegisteredCookie :: (Textual t, MonadHandler m, Yesod (HandlerSite m), HasCookieSettings RegisteredCookie (HandlerSite m)) => RegisteredCookie -> t -> m ()
setRegisteredCookie = setRegisteredCookie' id

setRegisteredCookieJson' :: (ToJSON v, MonadHandler m, Yesod (HandlerSite m), HasCookieSettings RegisteredCookie (HandlerSite m)) => (SetCookie -> SetCookie) -> RegisteredCookie -> v -> m ()
setRegisteredCookieJson' modSet name = setRegisteredCookie' modSet name . Aeson.encodeToLazyText

setRegisteredCookieJson :: (ToJSON v, MonadHandler m, Yesod (HandlerSite m), HasCookieSettings RegisteredCookie (HandlerSite m)) => RegisteredCookie -> v -> m ()
setRegisteredCookieJson = setRegisteredCookieJson' id

modifyRegisteredCookieJson' :: (FromJSON v, ToJSON v, MonadHandler m, Yesod (HandlerSite m), HasCookieSettings RegisteredCookie (HandlerSite m)) => (SetCookie -> SetCookie) -> RegisteredCookie -> ([v] -> Maybe v) -> m ()
modifyRegisteredCookieJson' modSet name modVal = lookupRegisteredCookiesJson pure name >>= maybe deleteRegisteredCookie'' (setRegisteredCookieJson' modSet name) . modVal
  where deleteRegisteredCookie'' = do
          path <- getCookiePath
          let cookieSettings = modSet def{ setCookiePath = Just path }
          deleteRegisteredCookie' name . maybe "/" Text.decodeUtf8 $ setCookiePath cookieSettings

modifyRegisteredCookieJson :: (FromJSON v, ToJSON v, MonadHandler m, Yesod (HandlerSite m), HasCookieSettings RegisteredCookie (HandlerSite m)) => RegisteredCookie -> ([v] -> Maybe v) -> m ()
modifyRegisteredCookieJson = modifyRegisteredCookieJson' id

tellRegisteredCookieJson' :: (FromJSON v, ToJSON v, MonadHandler m, Yesod (HandlerSite m), HasCookieSettings RegisteredCookie (HandlerSite m), Monoid v) => (SetCookie -> SetCookie) -> RegisteredCookie -> v -> m ()
tellRegisteredCookieJson' modSet name x = modifyRegisteredCookieJson' modSet name $ pure . (<> x) . fold

tellRegisteredCookieJson :: (FromJSON v, ToJSON v, MonadHandler m, Yesod (HandlerSite m), HasCookieSettings RegisteredCookie (HandlerSite m), Monoid v) => RegisteredCookie -> v -> m ()
tellRegisteredCookieJson = tellRegisteredCookieJson' id

-- Primitive
deleteRegisteredCookie' :: MonadHandler m
                        => RegisteredCookie -- ^ key
                        -> Text -- ^ path
                        -> m ()
deleteRegisteredCookie' (toPathPiece -> name) path = do
  deleteCookie name path
  cacheBySet (Text.encodeUtf8 name) $ RegisteredCookieCurrentValue Nothing

deleteRegisteredCookie :: (MonadHandler m, Yesod (HandlerSite m)) => RegisteredCookie -> m ()
deleteRegisteredCookie name = deleteRegisteredCookie' name . Text.decodeUtf8 =<< getCookiePath

-- Primitive
lookupRegisteredCookies :: (Textual t, Monoid m, MonadHandler f) => (t -> m) -> RegisteredCookie -> f m
lookupRegisteredCookies toM (toPathPiece -> name) = do
  cachedVal <- cacheByGet (Text.encodeUtf8 name)
  case cachedVal of
    Nothing
      -> foldMap (toM . repack) . mapMaybe (preview _CookieEncoded) <$> lookupCookies name
    Just (RegisteredCookieCurrentValue v)
      -> return . maybe mempty (toM . repack) $ v ^? _Just . _CookieEncoded

lookupRegisteredCookie :: (Textual t, MonadHandler m) => RegisteredCookie -> m (Maybe t)
lookupRegisteredCookie = fmap getLast . lookupRegisteredCookies pure

lookupRegisteredCookiesJson :: (FromJSON v, Monoid m, MonadHandler f) => (v -> m) -> RegisteredCookie -> f m
lookupRegisteredCookiesJson toM = fmap (fromMaybe mempty) . lookupRegisteredCookies (fmap toM . Aeson.decodeStrict' . Text.encodeUtf8)

lookupRegisteredCookieJson :: (FromJSON v, MonadHandler m) => RegisteredCookie -> m (Maybe v)
lookupRegisteredCookieJson = fmap getLast . lookupRegisteredCookiesJson pure
