{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleContexts #-}

-- |
--
-- Module for interface of the ATND JSON API.
--
-- Example usage to get events (in IO monad):
-- 
-- > cfg <- defaultATNDConfig
-- > runATND cfg $ do
-- >   getEvents <[eventId]> <[keywords]> ... Nothing ...
-- > 
-- 
-- To run in a monad that implements MonadIO, MonadLogger and
-- MonadBaseControl IO (such as a Yesod Handler), use ATNDM.
--
module Web.ATND 
    (
      -- * The ATND/ATNDT monad 
      ATND,
      ATNDT,
      runATND,
      runATNDT,
      -- * Running query
      query,
      query',
      -- * Config
      ATNDConfig(..),
      defaultATNDConfig,
      -- * Error Handling
      ATNDError 
    ) where

import Web.ATND.Util

import Data.Text(Text, unpack, pack)
import qualified Data.Text as D (concat)
import Data.Typeable

import Data.Aeson (decode, Value(..), FromJSON(..), (.:))

import Control.Monad (mzero)
import Control.Monad.IO.Class (liftIO, MonadIO)
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Lazy as BL
import Network.HTTP.Conduit (HttpException(..), setQueryString, tlsManagerSettings, parseUrl, newManager, httpLbs, Response(..), Request(..), Manager)
import Network.HTTP.Types.Header (ResponseHeaders)
import Control.Monad.Reader (runReaderT, ReaderT)
import Control.Monad.Logger
import Control.Monad.Trans.Control (MonadBaseControl)
import Control.Exception.Lifted as CEL

-- | Represent the config for ATND/ATNDT
data ATNDConfig = ATNDConfig { atndManager :: Manager }

-- | Create a ATNDConfig with a new Manager
defaultATNDConfig :: MonadIO m => m ATNDConfig
defaultATNDConfig = do
      man <- liftIO $ newManager tlsManagerSettings
      return ATNDConfig { atndManager = man }

-- | ATND monad transformer
type ATNDT m a = (MonadIO m, MonadLogger m, MonadBaseControl IO m) => ReaderT ATNDConfig m a

-- | Run ATNDT
runATNDT :: (MonadIO m, MonadLogger m, MonadBaseControl IO m) => ATNDConfig -> ATNDT m a -> m a
runATNDT config action =
    runReaderT action config

-- | Alias of ATNDT with Logging 
type ATND a = ATNDT(LoggingT IO) a 

-- | Run ATND in IO, ignoring the existing monadic context and logging to stderr.
runATND :: (MonadIO m) => 
  (ATNDConfig -> ATNDT(LoggingT IO) a -> m a)
runATND config action =
    liftIO $ runStderrLoggingT $ runATNDT config action

-- | Build the ATND endpoint URL
-- make https://api.atnd.org/events/[usres]
endpointUrl :: Section -> Text
endpointUrl a = D.concat ["https://api.atnd.org/", fromSection a] 

-- | Run a query to ATND. Remove Nothing in the query list. 
query :: (FromJSON x) => Section 
       -- ^ ATND api type
       -> [(B8.ByteString, Maybe B8.ByteString)] 
       -- ^ List for Query String  
       -> ATNDT m x
query section queryList = do
       query' section $ filterQuery queryList 

-- | Run a query to ATND, for apply query list directry.
query' :: (FromJSON x) => Section -> [(B8.ByteString, Maybe B8.ByteString)] -> ATNDT m x
query' section queryList = do 
       config <- defaultATNDConfig 
       initReq <- liftIO $ parseUrl $ unpack $ endpointUrl section 
       let req = setQueryString queryList  $ initReq
       $(logDebug) $ pack . show $ queryString req 
       response <- CEL.catch (httpLbs req $ atndManager config) catchHttpException
       $(logDebug) $ pack . show $ responseBody response 
       case decode $ responseBody response of
         Just eventResults -> return eventResults
         Nothing -> throwIO $ OtherATNDError (-1) "Parse Error: Could not parse result JSON from ATND"
       where
         catchHttpException :: HttpException -> ATNDT m a
         catchHttpException e@(StatusCodeException _ headers _) = do
           $(logDebug) $ pack . show $ lookup "X-Response-Boby-Start" headers 
           maybe (throwIO e) throwIO (decodeError headers)
         catchHttpException e = throwIO e
         decodeError :: ResponseHeaders -> Maybe ATNDError
         decodeError headers = BL.fromStrict `fmap` lookup "X-Response-Body-Start" headers >>= decode

-- | Error of ATND API
data ATNDError = NotFoundError Int Text
               | OtherATNDError Int Text
               deriving (Typeable, Show, Eq)

instance Exception ATNDError

instance FromJSON ATNDError where
    parseJSON (Object v) = do
      status <- v .: "status"
      message <- v .: "error"
      return $ (errConstructor status) ((read $ unpack status)::Int) message
     where
       errConstructor status = case (status :: Text) of
                                 "404" -> NotFoundError
                                 _ -> OtherATNDError
    parseJSON _ = mzero