{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DeriveDataTypeable    #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE QuasiQuotes           #-}
{-# LANGUAGE RecordWildCards       #-}
{-# LANGUAGE TemplateHaskell       #-}

-- | Function for running arbitrary IDE API commands.

module FP.API.Run
  where

import           FP.API.Convert
import           FP.API.Types

import           Control.Exception as E
import           Control.Failure
import           Control.Monad.Extra
import           Control.Monad.Logger
import           Control.Monad.Reader
import           Control.Monad.Trans.Resource
import           Data.Aeson                   (decode, encode)
import           Data.ByteString.Lazy         (toChunks)
import           Data.Data
import           Data.IORef
import           Data.Monoid
import           Data.Text                    (Text)
import qualified Data.Text                    as T
import           Data.Text.Encoding           (encodeUtf8)
import           Language.Fay.Yesod           (Returns (..))
import           Network.HTTP.Conduit
import           Network.HTTP.Types.Status
import           Prelude
import           Texts.English

-- | Only used internally for encoding.
data Command = IdeCommand !IdeCommand
  deriving Show

-- | Monad that can get info for sending commands.
class (Failure HttpException m,MonadIO m,Functor m) => MonadClient m where
  getClientConfig :: m ClientConfig

-- | Simple command configuration.
data ClientConfig = CC
  { ccUrl       :: !Text
  , ccToken     :: !Text
  , ccManager   :: !Manager
  , ccCookie    :: !(IORef CookieJar)
  , ccUserAgent :: !Text
  }

data CommandException
  = CommandException Text
   deriving (Show,Typeable)
instance Exception CommandException

-- | Helpful simple client instance.
instance (Failure HttpException m,MonadIO m,Functor m) => MonadClient (ReaderT ClientConfig m) where
  getClientConfig = ask

-- | Run the given command.
runCommand :: (MonadLogger m,MonadClient m,Data a,Show a) => (Returns' a -> IdeCommand) -> m a
runCommand cmd = do
  CC{..} <- getClientConfig
  request <- parseUrl (T.unpack (ccUrl <> "/fay-command"))
  jar <- io $ readIORef ccCookie
  let req = urlEncodedBody params $ setup ccToken request (Just jar) ccUserAgent
  $(logDebug) ("=> " <> trunc (T.pack (show (IdeCommand (cmd Returns)))))
  resp <- io (runResourceT (httpLbs req ccManager))
  io $ writeIORef ccCookie (responseCookieJar resp)
  case statusCode (responseStatus resp) of
    200 -> do
      case decode (responseBody resp) >>= readFromFay' of
        Nothing -> do $(logDebug) (errprefix <> "<= [unable to decode] " <> T.pack (show (responseBody resp)))
                      error $ "Unable to decode response: " ++ show (responseBody resp)
        Just result ->
          case result of
            Failure e -> do $(logDebug) (errprefix <> "<= " <> trunc e)
                            throw (CommandException e)
            Success a -> do $(logDebug) ("<= " <> trunc (T.pack (show a)))
                            return a
    code -> throw (CommandException ("Bad status code returned from client command: " <> T.pack (show code)))

  where params =
          [("json",mconcat (toChunks (encode (showToFay' (IdeCommand (cmd Returns))))))]
        setup token req jar agent =
          req { method = "POST"
              , requestHeaders = requestHeaders req ++
                                 [("Accept","application/json")
                                 ,("User-Agent",encodeUtf8 ("fpco-api:" <> agent))
                                 ,("authorization",encodeUtf8 ("token " <> token))]
              , responseTimeout = Nothing
              , cookieJar = jar
              }
        trunc :: Text -> Text
        trunc = ellipsize 140
        errprefix = "Error from request: " <> T.pack (show (cmd Returns)) <> "\n"