{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE LambdaCase             #-}
{-# LANGUAGE MultiParamTypeClasses  #-}
{-# LANGUAGE OverloadedStrings      #-}
{-# LANGUAGE RecordWildCards        #-}
{-# LANGUAGE ScopedTypeVariables    #-}

{-| This module contains conversion operations that have the potential to fail.

    Such conversions are typically used when processing messages received from
    a DocuSign server instance, which may return values that are incomplete.

    Such messages typically include record instances for which only a subset of
    values are present. The precise subset of values included often depends on
    which particular operation was invoked, or on the current server state.

    The knowledge of when to expect that values will be present (and when to
    expect that they will be missing) is encapsulated in this module.
-}

module DocuSign.Client.Types.Parsing where

import DocuSign.Client.Types

import Control.Exception              ( Exception )
import Control.Monad                  ( join )
import Control.Monad.Catch            ( MonadThrow
                                      , throwM )
import Type.Reflection                ( TypeRep
                                      , typeRep )
import Data.Text                      ( Text )
import Data.Text.Read                 ( decimal )
import Data.Typeable                  ( Typeable )
import Data.UUID                      ( UUID )

import qualified Data.Text               as T
import qualified Data.UUID               as U
import qualified DocuSign.Base.Types as D

-- | A class for conversions that have the potential to fail.
class Parse a b where
  parse :: a -> Maybe b

-- | Parse a value monadically.
parseM :: forall a b m .
  MonadThrow m   =>
  Parse      a b =>
  Show       a   =>
  Typeable   a   =>
  Typeable   b   => a -> m b
parseM a = maybe (throwM (parseFailure a :: ParseFailure a b)) pure $ parse a

-- | Thrown when the base API fails to return a value that was expected.
data ParseFailure a b = ParseFailure
  { sourceValue :: a
  , sourceType  :: TypeRep a
  , targetType  :: TypeRep b }
  deriving Show

parseFailure :: Typeable a => Typeable b => a -> ParseFailure a b
parseFailure a = ParseFailure a typeRep typeRep

instance (Show a, Typeable a, Typeable b) => Exception (ParseFailure a b)

instance Parse D.Authentication [AccountInfo] where
  parse D.Authentication {..} =
    join $ fmap (traverse parse) authenticationLoginAccounts

instance Parse Text AccountId where
  parse = either (const Nothing) (Just . fst) . decimal

instance Parse D.LoginAccount AccountInfo where
  parse D.LoginAccount {..} =
    AccountInfo
      <$> (parse         =<< loginAccountAccountId)
      <*> (parse         =<< loginAccountIsDefault)
      <*> (mkAccountName <$> loginAccountName     )
      <*> (mkUserId      <$> loginAccountUserId   )
      <*> (mkUserName    <$> loginAccountUserName )

instance Parse Text UUID where
  parse = U.fromText

instance Parse Text EnvelopeId where
  parse = fmap mkEnvelopeId . parse

instance Parse D.EnvelopeSummary EnvelopeId where
  parse D.EnvelopeSummary {..} =
    parse =<< envelopeSummaryEnvelopeId

instance Parse D.EnvelopeViews Uri where
  parse D.EnvelopeViews {..} =
    mkUri <$> envelopeViewsUrl

instance Parse Text Bool where
  parse t = case T.unpack $ T.toLower t of
    't' : _ -> Just True
    'f' : _ -> Just False
    _       -> Nothing

instance Parse Text SigningEvent where
  parse = \case
    "cancel"            -> Just SigningCancelled
    "decline"           -> Just SigningDeclined
    "exception"         -> Just SigningException
    "fax_pending"       -> Just SigningFaxPending
    "id_check_failed"   -> Just SigningIdCheckFailed
    "session_timeout"   -> Just SigningSessionExpired
    "signing_complete"  -> Just SigningCompleted
    "ttl_expired"       -> Just SigningTokenExpired
    "viewing_complete"  -> Just SigningViewingCompleted
    _                   -> Nothing