{-# LANGUAGE GeneralizedNewtypeDeriving
           , UndecidableInstances
  #-}
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}

module Mail
  ( -- * Structured MIME emails
    module Network.Mail.Mime
    -- * MailT
  , MailT, defMailT
  , MailSmtpData(..), _smtpEnvelopeFrom, _smtpRecipients
  , _MailSmtpDataSet
  , MailContext(..)
  , MonadMail(..)
  , getMailMessageRender, getMailMsgRenderer
    -- * YesodMail
  , YesodMail(..)
  , MailException(..)
    -- * Monadically constructing Mail
  , PrioritisedAlternatives
  , ToMailPart(..)
  , addAlternatives, provideAlternative, providePreferredAlternative
  , addPart, addPart', modifyPart, partIsAttachment
  , MonadHeader(..)
  , MailHeader
  , MailObjectId
  , replaceMailHeader, addMailHeader, removeMailHeader, getMailHeaders, lookupMailHeader
  , replaceMailHeaderI, addMailHeaderI
  , setSubjectI
  , setMailObjectUUID, setMailObjectIdRandom, setMailObjectIdCrypto, setMailObjectIdPseudorandom
  , getMailObjectId
  , setDate, setDateCurrent
  , getMailSmtpData
  , _addressName, _addressEmail
  , _mailFrom, _mailTo, _mailCc, _mailBcc, _mailHeaders, _mailHeader, _mailParts
  , _partType, _partEncoding, _partDisposition, _partFilename, _partHeaders, _partContent
  ) where

import ClassyPrelude.Yesod hiding (snoc, (.=), getMessageRender, derivePersistFieldJSON)

import Data.Kind (Type)

import Model.Types.Languages
import Model.Types.Csv

import Network.Mail.Mime hiding (addPart, addAttachment)
import qualified Network.Mail.Mime as Mime (addPart)

import Settings.Mime

import Data.Monoid (Last(..))
import Control.Monad.Trans.RWS (RWST(..))
import Control.Monad.Trans.State (StateT(..), execStateT, mapStateT)
import Control.Monad.Trans.Writer (execWriter, execWriterT, Writer)
import Control.Monad.RWS.Class (MonadWriter(..), MonadState(..), modify)
import Control.Monad.Fail
import Control.Monad.Base
import Control.Monad.Catch

import Generics.Deriving.Monoid (memptydefault, mappenddefault)

import qualified Data.Sequence as Seq

import qualified Data.Set as Set

import qualified Data.Foldable as Foldable

import qualified Data.Text as Text
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Builder as LTB
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString as BS

import Utils (MsgRendererS(..), MonadSecretBox(..), YamlValue)
import Utils.Lens.TH

import Control.Lens hiding (from)
import Control.Lens.Extras (is)

import Text.Blaze.Renderer.Utf8

import Data.UUID (UUID)
import qualified Data.UUID as UUID
import Data.UUID.Cryptographic.ImplicitNamespace

import Data.Binary (Binary)
import qualified Data.Binary as Binary

import "network-bsd" Network.BSD (getHostName)

import Data.Time.Zones (utcTZ, utcToLocalTimeTZ, timeZoneForUTCTime)
import Data.Time.LocalTime (ZonedTime(..), TimeZone(..))
import Data.Time.Format (rfc822DateFormat)

import Network.HaskellNet.SMTP (SMTPConnection)
import qualified Network.HaskellNet.SMTP as SMTP

import qualified Text.Hamlet as Hamlet (Translate)
import qualified Text.Shakespeare as Shakespeare (RenderUrl)

import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Encode.Pretty as Aeson
import qualified Data.Yaml as Yaml
import Data.Aeson.TH
import Utils.PathPiece (splitCamel)
import Utils.DateTime

import Data.Universe.Instances.Reverse ()
import Data.Universe.Instances.Reverse.JSON ()
import Data.Universe.Instances.Reverse.Hashable ()

import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI

import Control.Monad.Random (MonadRandom(..))
import qualified Crypto.Saltine.Class as Saltine (IsEncoding(..))
import qualified Data.ByteArray as ByteArray (convert)
import Crypto.MAC.KMAC (KMAC)
import qualified Crypto.MAC.KMAC as KMAC
import Crypto.Hash.Algorithms (SHAKE128)

import Language.Haskell.TH (nameBase)

import Network.Mail.Mime.Instances()


makeLenses_ ''Address
makeLenses_ ''Mail
makeLenses_ ''Part

_partFilename :: Traversal' Part Text
_partFilename = _partDisposition . dispositionFilename
  where
    dispositionFilename :: Traversal' Disposition Text
    dispositionFilename f (AttachmentDisposition t) = AttachmentDisposition <$> f t
    dispositionFilename f (InlineDisposition     t) = InlineDisposition     <$> f t
    dispositionFilename _  DefaultDisposition       = pure DefaultDisposition

_mailHeader :: CI ByteString -> Traversal' Mail Text
_mailHeader hdrName = _mailHeaders . traverse . filtered (views _1 $ (== hdrName) . CI.mk) . _2


newtype MailT m a = MailT { _unMailT :: RWST MailContext MailSmtpData Mail m a }
  deriving newtype ( MonadTrans, Monad, Functor, MonadFail, Applicative, Alternative, MonadPlus
                   , MonadIO, MonadLogger, MonadHandler, MonadCatch, MonadThrow, MonadMask, MonadResource, MonadBase b
                   , MonadState Mail, MonadWriter MailSmtpData, MonadReader MailContext
                   )

instance {-# OVERLAPPING #-} (MonadCrypto m, MonadCryptoKey m ~ CryptoIDKey) => MonadCrypto (MailT m) where
  type MonadCryptoKey (MailT m) = CryptoIDKey
  cryptoIDKey f = lift (cryptoIDKey return) >>= f

data MailSmtpData = MailSmtpData
  { smtpEnvelopeFrom :: Last Text
  , smtpRecipients :: Set Address
  } deriving (Eq, Ord, Show, Read, Generic)

instance Semigroup MailSmtpData where
  (<>) = mappenddefault

instance Monoid MailSmtpData where
  mempty = memptydefault
  mappend = (<>)

_MailSmtpDataSet :: Getter MailSmtpData Bool
_MailSmtpDataSet = to $ \MailSmtpData{..} -> none id
  [ is (_Wrapped . _Nothing) smtpEnvelopeFrom
  , Set.null smtpRecipients
  ]


data MailContext = MailContext
  { mcLanguages :: Languages
  , mcDateTimeFormat :: SelDateTimeFormat -> DateTimeFormat
  , mcCsvOptions :: CsvOptions
  } deriving (Eq, Ord, Read, Show, Generic, Typeable)

deriveJSON defaultOptions
  { fieldLabelModifier = intercalate "-" . map toLower . drop 1 . splitCamel
  } ''MailContext

instance Hashable MailContext
instance NFData MailContext
instance Default MailContext where
  def = MailContext
    { mcLanguages = def
    , mcDateTimeFormat = def
    , mcCsvOptions = def
    }

makeLenses_ ''MailContext

makeLenses_ ''MailSmtpData

class (MonadHandler m, MonadState Mail m) => MonadMail m where
  askMailLanguages :: m Languages
  askMailDateTimeFormat :: SelDateTimeFormat -> m DateTimeFormat
  askMailCsvOptions :: m CsvOptions
  tellMailSmtpData :: MailSmtpData -> m ()

instance MonadHandler m => MonadMail (MailT m) where
  askMailLanguages = view _mcLanguages
  askMailDateTimeFormat = (view _mcDateTimeFormat ??)
  askMailCsvOptions = view _mcCsvOptions
  tellMailSmtpData = tell

getMailMessageRender :: ( MonadMail m
                        , HandlerSite m ~ site
                        , RenderMessage site msg
                        ) => m (msg -> Text)
getMailMessageRender = renderMessage <$> getYesod <*> (view _Wrapped <$> askMailLanguages)

getMailMsgRenderer :: forall site m.
                      ( MonadMail m
                      , HandlerSite m ~ site
                      ) => m (MsgRendererS site)
getMailMsgRenderer = do
  mr <- getMailMessageRender
  return $ MsgRenderer (mr . SomeMessage :: RenderMessage site msg => msg -> Text)


data MailException = MailNotAvailable
                   | MailNoSenderSpecified
                   | MailNoRecipientsSpecified
  deriving (Eq, Ord, Read, Show, Generic, Typeable)

instance Exception MailException


class Yesod site => YesodMail site where
  defaultFromAddress :: (MonadHandler m, HandlerSite m ~ site) => m Address
  defaultFromAddress = Address Nothing . ("yesod@" <>) . pack <$> liftIO getHostName
  envelopeFromAddress :: (MonadHandler m, HandlerSite m ~ site) => m Text
  envelopeFromAddress = addressEmail <$> defaultFromAddress

  mailObjectIdDomain :: (MonadHandler m, HandlerSite m ~ site) => m Text
  mailObjectIdDomain = pack <$> liftIO getHostName

  mailDateTZ :: (MonadHandler m, HandlerSite m ~ site) => m TZ
  mailDateTZ = return utcTZ

  mailSmtp :: ( MonadHandler m
              , HandlerSite m ~ site
              , MonadUnliftIO m
              , MonadThrow m
              ) => (SMTPConnection -> m a) -> m a
  mailSmtp _ = throwM MailNotAvailable

  mailT :: ( MonadHandler m
           , HandlerSite m ~ site
           , MonadUnliftIO m
           , MonadLogger m
           , MonadThrow m
           ) => MailContext -> MailT m a -> m a

  default mailT :: ( MonadHandler m
                   , HandlerSite m ~ site
                   , MonadUnliftIO m
                   , MonadThrow m
                   ) => MailContext -> MailT m a -> m a
  mailT = defMailT

  defaultMailLayout :: ( MonadHandler m
                       , HandlerSite m ~ site
                       )
                    => WidgetFor site () -> m Html
  defaultMailLayout wgt = do
    PageContent{..} <- liftHandler $ widgetToPageContent wgt
    msgs <- getMessages
    withUrlRenderer [hamlet|
      $newline never
      $doctype 5
      <html>
        <head>
          <title>#{pageTitle}
          ^{pageHead}
        <body>
          $forall (status, msg) <- msgs
            <p class="message #{status}">#{msg}
        ^{pageBody}
    |]

defMailT :: ( MonadHandler m
            , YesodMail (HandlerSite m)
            , MonadUnliftIO m
            , MonadThrow m
            ) => MailContext
              -> MailT m a
              -> m a
defMailT ls (MailT mailC) = do
  fromAddress <- defaultFromAddress
  (ret, mail, smtpData) <- runRWST mailC ls (emptyMail fromAddress)
  mail' <- liftIO $ LBS.toStrict <$> renderMail' mail
  -- logDebugS "Mail" $ "Rendered mail:\n" <> decodeUtf8 mail'
  ret <$ case smtpData of
    MailSmtpData{ smtpEnvelopeFrom = Last Nothing } -> throwM MailNoSenderSpecified
    MailSmtpData{ smtpRecipients }
      | Set.null smtpRecipients -> throwM MailNoRecipientsSpecified
    MailSmtpData{ smtpEnvelopeFrom = Last (Just (unpack -> returnPath))
                , smtpRecipients = (map (unpack . addressEmail) . toList -> recipients)
                } -> mailSmtp $ \conn -> do
      $logInfoS "Mail" $ "Submitting email: " <> tshow smtpData
      liftIO $ SMTP.sendMail
        returnPath
        recipients
        mail'
        conn


data PrioritisedAlternatives m = PrioritisedAlternatives
  { preferredAlternative :: Last (m Part)
  , otherAlternatives :: Seq (m Part)
  } deriving (Generic)

instance Semigroup (PrioritisedAlternatives m) where
  (<>) = mappenddefault

instance Monoid (PrioritisedAlternatives m) where
  mempty = memptydefault
  mappend = (<>)

class YesodMail site => ToMailPart site a where
  type MailPartReturn site a :: Type
  type MailPartReturn site a = ()
  toMailPart :: (MonadMail m, HandlerSite m ~ site) => a -> StateT Part m (MailPartReturn site a)

instance YesodMail site => ToMailPart site (StateT Part (HandlerFor site) a) where
  type MailPartReturn site (StateT Part (HandlerFor site) a) = a
  toMailPart = mapStateT liftHandler

instance YesodMail site => ToMailPart site LT.Text where
  toMailPart text = do
    _partType .= decodeUtf8 typePlain
    _partEncoding .= QuotedPrintableText
    _partContent .= PartContent (encodeUtf8 text)

instance YesodMail site => ToMailPart site Text where
  toMailPart = toMailPart . LT.fromStrict

instance YesodMail site => ToMailPart site LTB.Builder where
  toMailPart = toMailPart . LTB.toLazyText

instance YesodMail site => ToMailPart site Html where
  toMailPart html = do
    _partType .= decodeUtf8 typeHtml
    _partEncoding .= QuotedPrintableText
    _partContent .= PartContent (renderMarkup html)

instance (ToMailPart site a, RenderMessage site msg) => ToMailPart site (Hamlet.Translate msg -> a) where
  type MailPartReturn site (Hamlet.Translate msg -> a) = MailPartReturn site a
  toMailPart act = do
    mr <- lift getMailMessageRender
    toMailPart $ act (toHtml . mr)

instance (ToMailPart site a, site ~ site') => ToMailPart site (MsgRendererS site' -> a) where
  type MailPartReturn site (MsgRendererS site' -> a) = MailPartReturn site a
  toMailPart act = do
    mr <- lift getMailMsgRenderer
    toMailPart $ act mr

instance ToMailPart site a => ToMailPart site (Shakespeare.RenderUrl (Route site) -> a) where
  type MailPartReturn site (Shakespeare.RenderUrl (Route site) -> a) = MailPartReturn site a
  toMailPart act = do
    ur <- getUrlRenderParams
    toMailPart $ act ur

instance YesodMail site => ToMailPart site Aeson.Value where
  toMailPart val = do
    _partType .= decodeUtf8 typeJson
    _partEncoding .= QuotedPrintableText
    _partContent .= PartContent (Aeson.encodePretty val)

instance YesodMail site => ToMailPart site YamlValue where
  toMailPart val = do
    _partType .= "text/vnd.yaml"
    _partEncoding .= QuotedPrintableText
    _partContent .= PartContent (fromStrict $ Yaml.encode val)


addAlternatives :: (MonadMail m)
                => Writer (PrioritisedAlternatives m) ()
                -> m ()
addAlternatives provided = do
  let PrioritisedAlternatives{..} = execWriter provided
  alternatives <- sequence . Foldable.toList $ maybe id (flip (Seq.|>)) (getLast preferredAlternative) otherAlternatives
  modify $ Mime.addPart alternatives

provideAlternative, providePreferredAlternative
  :: (MonadMail m, HandlerSite m ~ site, ToMailPart site a)
  => a
  -> Writer (PrioritisedAlternatives m) ()
provideAlternative part = tell $ mempty { otherAlternatives = Seq.singleton $ execStateT (toMailPart part) initialPart }
providePreferredAlternative part = tell $ mempty { preferredAlternative = Last . Just $ execStateT (toMailPart part) initialPart }

addPart :: ( MonadMail m
           , HandlerSite m ~ site
           , ToMailPart site a
           ) => a -> m (MailPartReturn site a)
addPart = addPart' . toMailPart

addPart' :: MonadMail m
         => StateT Part m a
         -> m a
addPart' part = do
  (ret, part') <- runStateT part initialPart
  modify . Mime.addPart $ pure part'
  return ret

initialPart :: Part
initialPart = Part
  { partType = decodeUtf8 defaultMimeType
  , partEncoding = Base64
  , partDisposition = DefaultDisposition
  , partHeaders = []
  , partContent = PartContent mempty
  }

modifyPart :: (MonadMail m, HandlerSite m ~ site, YesodMail site)
           => StateT Part (HandlerFor site) a
           -> StateT Part m a
modifyPart = toMailPart

partIsAttachment :: (Textual t, MonadMail m, HandlerSite m ~ site, YesodMail site)
                 => t
                 -> StateT Part m ()
partIsAttachment (repack -> fName) = modifyPart $ _partDisposition .= AttachmentDisposition fName


class MonadHandler m => MonadHeader m where
  stateHeaders :: forall a. (Headers -> (a, Headers)) -> m a
  modifyHeaders :: (Headers -> Headers) -> m ()
  modifyHeaders f = stateHeaders $ ((), ) . f
  objectIdHeader :: m MailHeader

instance MonadHandler m => MonadHeader (MailT m) where
  stateHeaders = MailT . zoom _mailHeaders . state
  objectIdHeader = return "Message-ID"

instance MonadHandler m => MonadHeader (StateT Part m) where
  stateHeaders = zoom _partHeaders . state
  objectIdHeader = return "Content-ID"


type MailHeader = ByteString
type MailObjectId = Text


replaceMailHeader :: MonadHeader m => MailHeader -> Maybe Text -> m ()
replaceMailHeader header mC = removeMailHeader header >> maybe (return ()) (addMailHeader header) mC

addMailHeader :: MonadHeader m => MailHeader -> Text -> m ()
addMailHeader header c = modifyHeaders $ \mailHeaders -> mailHeaders `snoc` (header, c)

removeMailHeader :: MonadHeader m => MailHeader -> m ()
removeMailHeader header = modifyHeaders $ \mailHeaders -> filter ((/= header) . fst) mailHeaders

getMailHeaders :: MonadHeader m => MailHeader -> m [Text]
getMailHeaders header = stateHeaders $ \hdrs -> (, hdrs) . map (view _2) $ filter (views _1 (== header)) hdrs

lookupMailHeader :: MonadHeader m => MailHeader -> m (Maybe Text)
lookupMailHeader = fmap listToMaybe . getMailHeaders


replaceMailHeaderI :: ( RenderMessage site msg
                      , MonadMail m
                      , HandlerSite m ~ site
                      , MonadHeader m
                      ) => MailHeader -> msg -> m ()
replaceMailHeaderI header msg = removeMailHeader header >> addMailHeaderI header msg

addMailHeaderI :: ( RenderMessage site msg
                  , MonadMail m
                  , HandlerSite m ~ site
                  , MonadHeader m
                  ) => MailHeader -> msg -> m ()
addMailHeaderI header msg = addMailHeader header =<< (getMailMessageRender <*> pure msg)


setSubjectI :: (RenderMessage site msg, MonadHandler m, HandlerSite m ~ site) => msg -> MailT m ()
setSubjectI = replaceMailHeaderI "Subject"

setMailObjectUUID :: ( MonadHeader m
                     , YesodMail (HandlerSite m)
                     ) => UUID -> m MailObjectId
setMailObjectUUID uuid = do
  domain <- mailObjectIdDomain
  oidHeader <- objectIdHeader
  let objectId = UUID.toText uuid <> "@" <> domain
  replaceMailHeader oidHeader . Just $ "<" <> objectId <> ">"
  return objectId

setMailObjectIdRandom :: ( MonadHeader m
                         , YesodMail (HandlerSite m)
                         ) => m MailObjectId
setMailObjectIdRandom = setMailObjectUUID =<< liftIO getRandom

setMailObjectIdCrypto :: ( MonadHeader m
                         , YesodMail (HandlerSite m)
                         , HasCryptoUUID plain m
                         ) => plain -> m MailObjectId
setMailObjectIdCrypto = setMailObjectUUID . ciphertext <=< encrypt

setMailObjectIdPseudorandom :: ( MonadHeader m
                               , YesodMail (HandlerSite m)
                               , Binary obj
                               , MonadSecretBox m
                               ) => obj -> m MailObjectId
-- | Designed to leak no information about the `secretBoxKey` or the given object
setMailObjectIdPseudorandom obj = do
  sbKey <- secretBoxKey
  let
    seed :: KMAC (SHAKE128 128)
    seed = KMAC.finalize . KMAC.updates (KMAC.initialize (BS.pack . encodeUtf8 $ nameBase 'setMailObjectIdPseudorandom) $ Saltine.encode sbKey) . LBS.toChunks $ Binary.encode obj
  setMailObjectUUID . fromMaybe (error "Could not convert hash to UUID") . UUID.fromByteString $ fromStrict (ByteArray.convert seed :: ByteString)

getMailObjectId :: ( MonadHeader m, YesodMail (HandlerSite m) ) => m (Maybe MailObjectId)
getMailObjectId = fmap (fmap stripBrackets) . lookupMailHeader =<< objectIdHeader
  where stripBrackets val = fromMaybe val $
          Text.stripSuffix ">" =<< Text.stripPrefix "<" val


setDateCurrent :: (MonadHandler m, YesodMail (HandlerSite m)) => MailT m ()
setDateCurrent = setDate =<< liftIO getCurrentTime

setDate :: (MonadHandler m, YesodMail (HandlerSite m)) => UTCTime -> MailT m ()
setDate time = do
  tz <- mailDateTZ
  let timeStr = formatTime defaultTimeLocale rfc822DateFormat $ ZonedTime (utcToLocalTimeTZ tz time) (rfc822zone $ timeZoneForUTCTime tz time)
  replaceMailHeader "Date" . Just $ pack timeStr
  where
    rfc822zone tz'
      | tz' `elem` rfc822zones = tz'
      | otherwise              = tz' { timeZoneName = "" }
    rfc822zones =
      [ TimeZone 0         False "UT"
      , TimeZone 0         False "GMT"
      , TimeZone (-5 * 60) False "EST"
      , TimeZone (-4 * 60) True  "EDT"
      , TimeZone (-6 * 60) False "CST"
      , TimeZone (-5 * 60) True  "CDT"
      , TimeZone (-7 * 60) False "MST"
      , TimeZone (-6 * 60) True  "MDT"
      , TimeZone (-8 * 60) False "PST"
      , TimeZone (-7 * 60) True  "PDT"
      ]


getMailSmtpData :: (MonadHandler m, YesodMail (HandlerSite m), MonadThrow m) => MailT m MailSmtpData
getMailSmtpData = execWriterT $ do
  from <- envelopeFromAddress
  recps <- lift $ Set.fromList . concat <$> forM [_mailTo, _mailCc, _mailBcc] use

  tell $ mempty
    { smtpRecipients = recps
    , smtpEnvelopeFrom = Last $ Just from 
    }
