{-# LANGUAGE BangPatterns      #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}

{-# OPTIONS_GHC -Wall #-}

module Azure.Email
  ( sendEmail
  , sendHtmlEmail
  , fetchAzureToken
  , Email(..)
  , AzureToken(..)
  , AzureCreds(..)
  ) where

import Codec.Crypto.RSA.Pure (PrivateKey)
import Control.Monad.Catch (MonadThrow, Exception, throwM)
import Data.Aeson ((.=),object)
import Data.Aeson.Lens (_String,key)
import Data.Function ((&))
import Data.Functor (void)
import Data.Monoid((<>))
import Data.Text (Text)
import Data.Time (UTCTime, addUTCTime, getCurrentTime)
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import Data.UUID (UUID)
import Lens.Micro ((^?),(.~))
import Network.HTTP.Client (Manager)
import Network.Wreq
import qualified Codec.Crypto.RSA.Pure as RSA
import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Base64.Lazy as Base64
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as Text
import qualified Data.Text.Encoding as TextEncoding
import qualified Data.UUID as UUID
import qualified Data.UUID.V4 as UUID4

data Email = Email
  { recipientAddress :: !Text
  , senderAddress    :: !Text
  , subject          :: !Text
  , body             :: !Text
  }

sendEmailInternal ::
     IsHtml
  -> Manager -- ^ HTTP manager
  -> AzureToken -- ^ token
  -> Email
  -> IO ()
sendEmailInternal isHtml mngr token (Email recipientEmail senderEmail subject' textBody) =
  void $ postWith opts
    (Text.unpack (Text.concat ["https://outlook.office365.com/api/v1.0/users/", senderEmail, "/sendmail"]))
    body'
  where
    opts = defaults
      & manager .~ Right mngr
      & header "Authorization" .~ [TextEncoding.encodeUtf8 $ "Bearer " <> getAzureToken token]
    body' = object
      [ "Message" .= object
        [ "Subject" .= subject'
        , "Body"    .= object
          [ "Content" .= textBody
          , "ContentType" .= isHtmlToContentType isHtml
          ]
        , "ToRecipients" .=
          [ object
            [ "EmailAddress" .= object
               [ "Address" .= recipientEmail
               ]
            ]
          ]
        ]
      , "SaveToSentItems" .= True
      ]

sendEmail ::
     Manager
  -> AzureToken
  -> Email
  -> IO ()
sendEmail = sendEmailInternal IsNotHtml

sendHtmlEmail ::
     Manager
  -> AzureToken
  -> Email
  -> IO ()
sendHtmlEmail = sendEmailInternal IsHtml


data IsHtml = IsHtml | IsNotHtml

isHtmlToContentType :: IsHtml -> Text
isHtmlToContentType = \case { IsHtml -> "HTML"; IsNotHtml -> "Text" }

asInt :: Int -> Int; asInt = id
asText :: Text -> Text; asText = id

buildAssertion :: PrivateKey -> Text -> UUID -> UUID -> UUID -> UTCTime -> Text
buildAssertion privateKey base64Fingerprint clientId tenantId requestId now =
  TextEncoding.decodeUtf8 $ BL.toStrict $ mempty
    <> encodedHeaderAndPayload
    <> "."
    <> Base64.encode ( fromRightErr "buildAssertion: RSA signing failed"
                       $ RSA.sign privateKey encodedHeaderAndPayload
                     )
  where
    encodedHeaderAndPayload = mempty
      <> Base64.encode (Aeson.encode theHeader)
      <> "."
      <> Base64.encode (Aeson.encode payload)
    theHeader = object
      [ "alg" .= asText "RS256"
      , "x5t" .= base64Fingerprint
      ]
    payload = object
      [ "aud" .= Text.concat ["https://login.microsoftonline.com/", uuidToText tenantId, "/oauth2/token"]
      , "sub" .= uuidToText clientId
      , "iss" .= uuidToText clientId
      , "jti" .= uuidToText requestId
      , "nbf" .= asInt (round $ utcTimeToPOSIXSeconds now)
      , "exp" .= asInt (round $ utcTimeToPOSIXSeconds $ addUTCTime 180 now)
      ]

uuidToText :: UUID -> Text
uuidToText = TextEncoding.decodeUtf8 . UUID.toASCIIBytes

fetchAzureToken :: Manager -> AzureCreds -> IO AzureToken
fetchAzureToken mngr creds@(AzureCreds clientId tenantId fingerPrint privateKey) = do
  let opts = defaults & manager .~ Right mngr
  now <- getCurrentTime
  requestId <- UUID4.nextRandom
  r <- postWith opts (Text.unpack (Text.concat ["https://login.microsoftonline.com/", uuidToText (azureTenantId creds), "/oatuh2/token"]))
         $ buildFormParams "https://outlook.office365.com/"
             privateKey
             fingerPrint
             clientId
             tenantId
             requestId
             now
  t <- throwFromJust "Could not get an access token" $ r ^? responseBody . key "access_token" . _String
  pure $ AzureToken t

buildFormParams :: Text -> PrivateKey -> Text -> UUID -> UUID -> UUID -> UTCTime -> [FormParam]
buildFormParams resource privateKey base64Fingerprint clientId tenantId requestId now =
  [ "grant_type"            := asText "client_credentials"
  , "resource"              := resource
  , "client_id"             := uuidToText clientId
  , "client_assertion_type" := asText "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
  , "client_assertion"      := buildAssertion privateKey base64Fingerprint clientId tenantId requestId now
  ]

newtype AzureToken = AzureToken { getAzureToken :: Text }

data AzureCreds = AzureCreds
  { azureClientId    :: !UUID
  , azureTenantId    :: !UUID
  , azureFingerprint :: !Text
  , azurePrivateKey  :: !PrivateKey
  }

fromRightErr :: String -> Either a b -> b
fromRightErr err = \case
  Left  _ -> error err
  Right b -> b

throwFromJust :: MonadThrow m => String -> Maybe a -> m a
throwFromJust err = \case
  Just a  -> pure a
  Nothing -> throwM (MyException err)

data MyException = MyException String
  deriving (Show, Eq)
instance Exception MyException