module Network.Pusher.Webhook
  ( Webhooks(..)
  , WebhookEv(..)
  , WebhookPayload(..)
  , parseAppKeyHdr
  , parseAuthSignatureHdr
  , parseWebhooksBody
  , verifyWebhooksBody
  , parseWebhookPayloadWith
  ) where

import qualified Crypto.Hash as HASH
import qualified Crypto.MAC.HMAC as HMAC
import Data.Aeson ((.:))
import qualified Data.Aeson as A
import Data.ByteArray
import qualified Data.ByteString.Base16 as B16
import qualified Data.ByteString.Char8 as BC
import Data.ByteString.Lazy (fromStrict)
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Char (toLower)
import Data.Function (on)
import Data.Maybe (mapMaybe)
import Data.Text (Text)
import Data.Text.Encoding
import Data.Time (UTCTime(..))
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Network.Pusher.Data
       (AppKey, AppSecret, Channel(..), SocketID)
import Network.Pusher.Internal.Auth (AuthSignature)
import Network.Pusher.Internal.Util
import Network.Pusher.Protocol (User(..))

-- |A Webhook is received by POST request from Pusher to notify your server of
-- a number of 'WebhookEv's. Multiple events are received under the same
-- timestamp if batch events is enabled.
data Webhooks = Webhooks
  { timeMs :: UTCTime
  , webhookEvs :: [WebhookEv]
  } deriving (Eq, Show)

instance A.FromJSON Webhooks where
  parseJSON o =
    case o of
      A.Object v ->
        Webhooks <$> (_unOurTime <$> A.parseJSON o) <*> (v .: "events")
      _ -> failExpectObj o

-- |Exists only so we can define our own 'FromJSON' instance on
-- 'NominalDiffTime'. This is useful because it didnt exist before a certain
-- GHC version that we support and allows us to avoid CPP and orphan instances.
newtype OurTime = OurTime
  { _unOurTime :: UTCTime
  }

instance A.FromJSON OurTime where
  parseJSON o =
    case o of
      A.Object v ->
        A.withScientific
          "NominalDiffTime"
          (pure . OurTime . posixSecondsToUTCTime . realToFrac) =<<
        v .: "time_ms"
      _ -> failExpectObj o

-- |A 'WebhookEv' is one of several events Pusher may send to your server in
-- response to events your users may trigger.
data WebhookEv
  -- |A channel has become occupied. There is > 1 subscriber.
  = ChannelOccupiedEv { onChannel :: Channel }
  -- |A channel has become vacated. There are 0 subscribers.
  | ChannelVacatedEv { onChannel :: Channel }
  -- |A new user has subscribed to a presence channel.
  | MemberAddedEv { onChannel :: Channel
                  , withUser :: User }
  -- |A user has unsubscribed from a presence channel.
  | MemberRemovedEv { onChannel :: Channel
                    , withUser :: User }
  -- |A client has sent a named client event with some json body. They have a
  -- 'SocketID' and a 'User' if they were in a presence channel.
  | ClientEv { onChannel :: Channel
             , clientEvName :: Text
             , clientEvBody :: Maybe A.Value
             , withSocketId :: SocketID
             , withPossibleUser :: Maybe User }
  deriving (Eq, Show)

instance A.FromJSON WebhookEv where
  parseJSON o =
    case o of
      A.Object v -> do
        name <- v .: "name"
        case name :: Text of
          "channel_occupied" -> ChannelOccupiedEv <$> v .: "channel"
          "channel_vacated" -> ChannelVacatedEv <$> v .: "channel"
          "member_added" ->
            MemberAddedEv <$> v .: "channel" <*> (User <$> v .: "user_id")
          "member_removed" ->
            MemberRemovedEv <$> v .: "channel" <*> (User <$> v .: "user_id")
          "client_event" ->
            ClientEv <$> v .: "channel" <*> v .: "event" <*>
            (A.decode . LB.fromStrict . encodeUtf8 <$> v .: "data") <*>
            v .: "socket_id" <*>
            (fmap User <$> v .: "user_id")
          _ -> fail . ("Unknown client event. Got: " ++) . show $ o
      _ -> failExpectObj o

data WebhookPayload = WebhookPayload {
    xPusherKey :: AppKey
  -- ^Authentication header. The oldest active token is used, identified by
  -- this key.
  , xPusherSignature :: AuthSignature
  -- ^A HMAC SHA256 formed by signing the payload with the tokens secret.
  , webhooks :: Webhooks
  } deriving (Eq, Show)

-- |Given a HTTP Header and its associated value, parse an 'AppKey'.
parseAppKeyHdr :: BC.ByteString -> BC.ByteString -> Maybe AppKey
parseAppKeyHdr key value
  | on (==) (BC.map toLower) key "X-Pusher-Key" = Just value
  | otherwise = Nothing

-- |Given a HTTP Header and its associated value, parse a 'AuthSignature'.
parseAuthSignatureHdr :: BC.ByteString -> BC.ByteString -> Maybe AuthSignature
parseAuthSignatureHdr key value
  | on (==) (BC.map toLower) key "X-Pusher-Signature" = Just value
  | otherwise = Nothing

-- |Given a HTTP body, parse the contained webhooks.
parseWebhooksBody :: BC.ByteString -> Maybe Webhooks
parseWebhooksBody = A.decode . fromStrict

-- |Does a webhook body hash with our secret key to the given signature?
verifyWebhooksBody :: AppSecret -> AuthSignature -> BC.ByteString -> Bool
verifyWebhooksBody appSecret authSignature body =
  let actualSignature =
        B16.encode $ convert (HMAC.hmac appSecret body :: HMAC.HMAC HASH.SHA256)
  in authSignature == actualSignature

safeHead :: [a] -> Maybe a
safeHead (x:_) = Just x
safeHead _ = Nothing

-- |Given a list of http header key:values, a http body and a lookup function
-- for an apps secret, parse and validate a  potential webhook payload.
parseWebhookPayloadWith ::
     (AppKey -> Maybe AppSecret)
  -> [(BC.ByteString, BC.ByteString)]
  -> BC.ByteString
  -> Maybe WebhookPayload
parseWebhookPayloadWith lookupKeysSecret headers body = do
  appKey <- safeHead . mapMaybe (uncurry parseAppKeyHdr) $ headers
  authSignature <- safeHead . mapMaybe (uncurry parseAuthSignatureHdr) $ headers
  appSecret <- lookupKeysSecret appKey
  () <-
    if verifyWebhooksBody appSecret authSignature body
      then Just ()
      else Nothing
  whs <- parseWebhooksBody body
  Just $ WebhookPayload appKey authSignature whs