{-# LANGUAGE OverloadedStrings #-}
module Snap.AtlassianConnect.PageToken
  ( PageToken(..) -- TODO make it so that you can query the token but nothing else
  , generateToken
  , generateTokenCurrentTime
  , encryptPageToken
  , decryptPageToken
  , defaultTimeoutSeconds
  ) where

import qualified Control.Applicative                  as CA
import qualified Crypto.Cipher.AES                    as CCA
import           Data.Aeson
import           Data.Aeson.Types
import qualified Data.ByteString                      as DB
import qualified Data.ByteString.Base64               as B64
import qualified Data.ByteString.Lazy                 as DBL
import qualified Data.Padding                         as DP
import qualified Data.Text                            as DT
import           Data.Time.Clock
import           Data.Time.Units

import qualified Snap.AtlassianConnect.AtlassianTypes as CA
import qualified Snap.AtlassianConnect.Tenant         as CT
import qualified Snap.AtlassianConnect.TimeUnits as TU

-- | The default timeout for a Page Token. It is likely that you will want to make this longer.
defaultTimeoutSeconds :: TU.ConnectSecond
defaultTimeoutSeconds = TU.CSecond $ 5 * 60

-- | Page Tokens are your way of providing a token to the client's browser and have them make trusted requests
-- back to the Atlassian Connect application without having to go through the host product.
data PageToken = PageToken
  { pageTokenHost                 :: CT.TenantKey     -- ^ The Atlassian Cloud tenant that this token was generated for.
  , pageTokenUser                 :: Maybe CA.UserKey -- ^ The potential user that this token was generated for.
  , pageTokenTimestamp            :: UTCTime          -- ^ The time at which this token was generated.
  , pageTokenAllowInsecurePolling :: Bool             -- ^ (Deprecated) This is currently unsupported. Don't use it.
  } deriving (Eq, Show)

instance ToJSON PageToken where
  toJSON (PageToken host potentialUser timestamp insecurePolling) = object $
    [ "h" .= host
    , "t" .= timestamp
    ]
    ++ user potentialUser
    ++ polling insecurePolling
    where
      user :: Maybe CA.UserKey -> [Pair]
      user (Just userKey)  = [ "u" .= userKey ]
      user Nothing      = []

      polling :: Bool -> [Pair]
      polling True  = [ "p" .= DT.pack "1" ] -- Unfortunately we need to write out a true
      polling False  = []

instance FromJSON PageToken where
  parseJSON (Object tokenData) = PageToken
    CA.<$> tokenData .: "h"
    CA.<*> tokenData .:? "u"
    CA.<*> tokenData .: "t"
    CA.<*> tokenData .:? "p" .!= False
  parseJSON _ = fail "The PageToken should contain a JSON object."

-- | Given a tentant, a potential user and a time generate a page token.
generateToken :: CT.TenantWithUser -> UTCTime -> PageToken
generateToken (tenant, userKey) timestamp = PageToken
  { pageTokenHost = CT.key tenant
  , pageTokenUser = userKey
  , pageTokenTimestamp = timestamp
  , pageTokenAllowInsecurePolling = False
  }

-- | Given a tenant and a potential user generate a token for the current time.
generateTokenCurrentTime :: CT.TenantWithUser -> IO PageToken
generateTokenCurrentTime ct = fmap (generateToken ct) getCurrentTime

-- In order to write the token out:
-- 1. Generate the token.
-- 1. Write the token out to json in the supported format.
-- 1. Base64 encode the json.
-- 1. AES ECB encrypt the string

-- | Given an AES context encrypt a page token into a bytestring.
encryptPageToken :: CCA.AES -> PageToken -> DB.ByteString
encryptPageToken aes pageToken = encryptedEncodedToken
  where
    tokenAsJson = encode pageToken
    tokenAsBase64 = B64.encode . DBL.toStrict $ tokenAsJson
    paddedBase64 = DP.zeroPad 16 tokenAsBase64
    encryptedToken = CCA.encryptECB aes paddedBase64
    encryptedEncodedToken = B64.encode encryptedToken

-- | Given an AES context decrypt a page token.
decryptPageToken :: CCA.AES -> DB.ByteString -> Either String PageToken
decryptPageToken aes input = do
  undecodedEncryptedToken <- B64.decode input
  let decryptedToken = CCA.decryptECB aes undecodedEncryptedToken
  (B64.decode . DP.zeroUnpad $ decryptedToken) >>= eitherDecode . DBL.fromStrict

-- TODO we should write unit tests to ensure that every combination of this encrypt and decrypt can be translated correctly.