module APIBuilder.API
  ( API 
  , liftEither
  , liftBuilder
  , liftState
  , runAPI
  , runRoute
  , routeRequest
  , name
  , baseURL
  , customizeRoute
  , customizeRequest ) where

import APIBuilder.Builder
import APIBuilder.Decoding
import APIBuilder.Error
import APIBuilder.Routes

import Control.Exception
import Control.Monad.Trans.Either
import Control.Monad.Trans.State
import Control.Monad.Trans.Class (lift)
import Control.Monad.IO.Class (liftIO)
import Data.Aeson (FromJSON)
import Data.Text (Text)
import qualified Data.ByteString.Char8 as BS
import qualified Data.Text as T
import Network.HTTP.Conduit

type API s e a = EitherT (APIError e) (StateT Builder (StateT s IO)) a

liftEither :: API s e a -> API s e a
liftEither = id 

liftBuilder :: StateT Builder (StateT s IO) a -> API s e a
liftBuilder = lift

liftState :: StateT s IO a -> API s e a
liftState = lift . lift

runAPI :: Builder -> s -> API s e a -> IO (Either (APIError e) a)
runAPI b s api = evalStateT (evalStateT (runEitherT api) b) s

runRoute :: (FromJSON a, FromJSON e) => Route -> API s e a
runRoute route = do
  b <- liftBuilder get
  req <- hoistEither $ routeRequest b route `eitherOr` InvalidURLError
  resp <- do
    r <- liftIO $ try $ withManager (httpLbs req)
    hoistEither $ either (Left . HTTPError) Right r 
  hoistEither $ decode $ responseBody resp

eitherOr :: Maybe a -> b -> Either b a
a `eitherOr` b =
  case a of 
    Just x -> Right x
    Nothing -> Left b

routeRequest :: Builder -> Route -> Maybe Request
routeRequest b route = 
  let initialURL = parseUrl (T.unpack $ routeURL (_baseURL b) (_customizeRoute b route)) in
  fmap (\url -> _customizeRequest b $ url { method = BS.pack (showMethod $ httpMethod route) }) initialURL

name :: Text -> API s e ()
name t = liftBuilder $ modify (\b -> b { _name = t })

baseURL :: Text -> API s e ()
baseURL t = liftBuilder $ modify (\b -> b { _baseURL = t })

customizeRoute :: (Route -> Route) -> API s e ()
customizeRoute f = liftBuilder $ modify (\b -> b { _customizeRoute = f })

customizeRequest :: (Request -> Request) -> API s e ()
customizeRequest f = liftBuilder $ modify (\b -> b { _customizeRequest = f })