module Reddit.Types.Reddit
  ( Reddit
  , RedditT(..)
  , nest
  , failWith
  , Modhash(..)
  , LoginDetails(..)
  , POSTWrapped(..)
  , ShouldRateLimit
  , RateLimits(RateLimits, should, info)
  , RateLimitInfo(..)
  , headersToRateLimitInfo
  , builder
  , mainBaseURL
  , loginBaseURL
  , addHeader
  , addAPIType ) where

import Reddit.Types.Error

import Control.Applicative
import Control.Concurrent.STM.TVar
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Trans.Except
import Control.Monad.Trans.Reader (ask)
import Control.Monad.Trans.State (get, put)
import Data.Aeson
import Data.Monoid
import Data.Text (Text)
import Data.Time.Clock
import Network.API.Builder
import Network.HTTP.Conduit hiding (path)
import Network.HTTP.Types
import Prelude
import Text.Read (readMaybe)
import qualified Data.ByteString.Char8 as BS
import qualified Data.Text as Text

type Reddit a = RedditT IO a

newtype RedditT m a = RedditT { unRedditT :: APIT (TVar RateLimits) RedditError m a }

instance Functor m => Functor (RedditT m) where
  fmap f (RedditT a) = RedditT (fmap f a)

instance (Functor m, Monad m) => Applicative (RedditT m) where
  pure a = RedditT (pure a)
  (RedditT f) <*> (RedditT a) = RedditT (f <*> a)

instance Monad m => Monad (RedditT m) where
  return a = RedditT (return a)
  (RedditT a) >>= f = RedditT (a >>= unRedditT . f)
  fail = failWith . APIError . FailError . Text.pack

instance MonadIO m => MonadIO (RedditT m) where
  liftIO a = RedditT (liftIO a)

instance MonadTrans RedditT where
  lift = RedditT . lift . lift . lift . lift

nest :: MonadIO m => RedditT m a -> RedditT m (Either (APIError RedditError) a)
nest (RedditT a) = do
  b <- RedditT $ liftBuilder get
  rl <- RedditT $ liftState get
  m <- RedditT $ liftManager ask
  (res, b', rl') <- lift $ runAPI b m rl a
  RedditT $ do
    liftBuilder $ put b'
    liftState $ put rl'
  return res

failWith :: Monad m => APIError RedditError -> RedditT m a
failWith = RedditT . throwE

newtype Modhash = Modhash Text
  deriving (Show, Read, Eq)

instance FromJSON Modhash where
  parseJSON (Object o) =
    Modhash <$> ((o .: "json") >>= (.: "data") >>= (.: "modhash"))
  parseJSON _ = mempty

data LoginDetails = LoginDetails Modhash CookieJar
  deriving (Show, Eq)

newtype POSTWrapped a = POSTWrapped a
  deriving (Show, Read, Eq)

instance Functor POSTWrapped where
  fmap f (POSTWrapped a) = POSTWrapped (f a)

data RateLimits =
  RateLimits { should :: ShouldRateLimit
             , info :: Maybe RateLimitInfo }
  deriving (Show, Read, Eq)

type ShouldRateLimit = Bool

data RateLimitInfo = RateLimitInfo { used :: Integer
                                   , remaining :: Integer
                                   , resetTime :: UTCTime }
  deriving (Show, Read, Eq)

headersToRateLimitInfo :: ResponseHeaders -> UTCTime -> Maybe RateLimitInfo
headersToRateLimitInfo hs now =
  RateLimitInfo <$> rlUsed <*> rlRemaining <*> rlResetTime'
  where (rlUsed, rlRemaining, rlResetTime) =
          trimap extract ("x-ratelimit-used", "x-ratelimit-remaining", "x-ratelimit-reset")
        rlResetTime' = fmap (\s -> addUTCTime (fromIntegral s) now) rlResetTime
        extract s = lookup s hs >>= readMaybe . BS.unpack
        trimap f (a, b, c) = (f a, f b, f c)

builder :: Builder
builder = Builder "Reddit"
                  mainBaseURL
                  addAPIType
                  (addHeader Nothing)

addHeader :: Maybe BS.ByteString -> Request -> Request
addHeader Nothing req = req { requestHeaders =
  ("User-Agent", "reddit-haskell 0.1.0.0 / intolerable") : requestHeaders req }
addHeader (Just hdr) req = req { requestHeaders =
  ("User-Agent", hdr) : requestHeaders req }

addAPIType :: Route -> Route
addAPIType (Route fs ps m) = Route fs ("api_type" =. ("json" :: Text) : ps) m

mainBaseURL :: Text
mainBaseURL = "https://api.reddit.com"

loginBaseURL :: Text
loginBaseURL = "https://ssl.reddit.com"