{-# LANGUAGE BangPatterns      #-}
{-# LANGUAGE DeriveFoldable    #-}
{-# LANGUAGE DeriveFunctor     #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}

module Cookie
  ( Cookie(..)
  , CookieContent(..)
  , SameSite(..)
  , defaultCookie
  , encodeCookie
  , decodeCookie
  ) where

import Chronos.Types (Datetime, Timespan)
import Data.Bifunctor (second)
import Data.Bool (bool)
import Data.ByteString (ByteString)
import Data.Coerce (coerce)
import Data.Hashable (Hashable)
import Data.Monoid (Monoid)
import Data.Semigroup ((<>))
import Data.Text (Text)
import Data.Time (UTCTime(UTCTime), formatTime, defaultTimeLocale)
import qualified Chronos as C
import qualified Chronos.Types as C
import qualified Data.List as L
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Text.Lazy.Builder as TL
import qualified Data.Text.Lazy.Builder.Int as TL
import qualified Data.Time as UTC

data CookieContent a = CookieContent
  { cookieContentName  :: !Text
  , cookieContentValue :: !a
  } deriving (Eq, Ord, Show, Functor, Foldable, Traversable)

data Cookie a = Cookie
  { cookieContent  :: !(CookieContent a)
  , cookieExpires  :: !(Maybe Datetime)
  , cookieMaxAge   :: !(Maybe Timespan)
  , cookieDomain   :: !(Maybe Text)
  , cookiePath     :: !(Maybe [Text])
  , cookieSecure   :: !Bool
  , cookieHttpOnly :: !Bool
  , cookieSameSite :: !(Maybe SameSite)
  } deriving (Eq, Ord, Show, Functor, Foldable, Traversable)

data SameSite
  = SameSiteLax
  | SameSiteStrict
  deriving (Eq, Ord, Show)

defaultCookie :: CookieContent a -> Cookie a
defaultCookie content = Cookie content Nothing Nothing Nothing Nothing False False Nothing

encodeCookie :: (a -> ByteString) -> Cookie a -> TL.Builder
encodeCookie encodeContent (Cookie (CookieContent name value) expires maxAge domain path secure httpOnly sameSite) =
  (mconcat . L.intersperse "; " . mconcat)
  [ [ TL.fromText name <> "=" <> (TL.fromText . TE.decodeUtf8 . encodeContent) value ]
  , maybeToList expires $ \ex -> "Expires=" <> TL.fromText (formatExpires $ datetimeToUTCTime ex)
  , maybeToList maxAge $ \ma -> "Max-Age=" <> TL.decimal (C.getTimespan ma `div` 1000000000)
  , maybeToList domain $ \d -> "Domain=" <> TL.fromText d
  , maybeToList path $ \p -> "Path=/" <> TL.fromText (T.intercalate "/" p)
  , bool [] ["Secure"] secure
  , bool [] ["HttpOnly"] httpOnly
  , maybeToList sameSite $ \case
      SameSiteLax -> "SameSite=Lax"
      SameSiteStrict -> "SameSite=Strict"
  ]
  where
    datetimeToUTCTime :: Datetime -> UTCTime
    datetimeToUTCTime dt@(C.Datetime _ (C.TimeOfDay h m n)) = UTCTime d undefined
      where
        dif = UTC.secondsToDiffTime $ fromIntegral (3600 * h + 60 * m + fromIntegral (n `div` 1000000000))
        d = UTC.ModifiedJulianDay $ fromIntegral $ C.getDay $ C.timeToDayTruncate $ C.datetimeToTime dt
    formatExpires :: UTCTime -> Text
    formatExpires = T.pack . formatTime defaultTimeLocale expiresFormat
    expiresFormat = "%a, %d-%b-%Y %X GMT"
    maybeToList m f = maybe [] ((:[]) . f) m

decodeCookie :: (Text -> Either Text a) -> Text -> Either Text (Cookie a)
decodeCookie decodeValue txt = case L.filter (\(x,_) -> x == "auth") (kvPairs txt) of
  ((name,value):_) ->
    flip fmap (decodeValue value) $ \v ->
      Cookie
        { cookieContent = CookieContent name v
        , cookieExpires = Nothing
        , cookieMaxAge = Nothing
        , cookieDomain = Nothing
        , cookiePath = Nothing
        , cookieSecure = False
        , cookieHttpOnly = False
        , cookieSameSite = Nothing
        }
  _ -> Left "Empty Cookie"
  where
    kvPairs = fmap (both (T.dropAround (==' ')) . breakOnDiscard "=") . L.filter (not . T.null) . T.splitOn ";"
    both f (x,y) = (f x, f y)
    breakOnDiscard b = second (T.drop 1) . T.breakOn b