{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UnicodeSyntax #-}
{-# LANGUAGE ViewPatterns #-}
-- | StackExchange API request manipulation routines
module Network.StackExchange.Request
  ( -- * Type
    Request, R(..), Auth(..), SE(..), Object(..)
    -- * Constructing requests
  , host, path, method, parse
  , query, token, key, site, filter, state, Scope(..), scope
  , client, redirectURI, secret, code
    -- * Internal wrapping/unwrapping
  , wrap, unwrap
    -- * Rendering
  , render
  ) where

import Data.Monoid
import GHC.TypeLits (Symbol)
import Prelude hiding (filter)
import Unsafe.Coerce (unsafeCoerce)

import           Control.Lens hiding (query)
import           Data.ByteString.Lazy (ByteString)
import           Data.Aeson (FromJSON)
import           Data.Aeson.Types (Value)
import           Data.Default (Default(..))
import           Data.Map (Map)
import qualified Data.Map as M
import           Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as T
import           Data.Text.Lazy.Builder (toLazyText)
import           Data.Text.Lazy.Builder.Int (decimal)


-- | Authentication
data Auth =
    RequireToken -- ^ Request requires auth_token
  | Ready -- ^ Request may be sent without auth_token


-- | SE response type
data Object =
    AccessToken -- ^ <https://api.stackexchange.com/docs/types/access-token>
  | AccountMerge -- ^ <https://api.stackexchange.com/docs/types/account-merge>
  | Answer -- ^ <https://api.stackexchange.com/docs/types/answer>
  | Badge -- ^  <https://api.stackexchange.com/docs/types/badge>
  | Comment -- ^ <https://api.stackexchange.com/docs/types/comment>
  | Error -- ^ <https://api.stackexchange.com/docs/types/error>
  | Event -- ^ <https://api.stackexchange.com/docs/types/event>
  | Filter -- ^ <https://api.stackexchange.com/docs/types/filter>
  | InboxItem -- ^ <https://api.stackexchange.com/docs/types/inbox-item>
  | Info -- ^ <https://api.stackexchange.com/docs/types/info>
  | NetworkUser -- ^ <https://api.stackexchange.com/docs/types/network-user>
  | Notification -- ^ <https://api.stackexchange.com/docs/types/notification>
  | Post -- ^ <https://api.stackexchange.com/docs/types/post>
  | Privilege -- ^ <https://api.stackexchange.com/docs/types/privilege>
  | Question -- ^ <https://api.stackexchange.com/docs/types/question>
  | QuestionTimeline -- ^ <https://api.stackexchange.com/docs/types/question-timeline>
  | Reputation -- ^ <https://api.stackexchange.com/docs/types/reputation>
  | ReputationHistory -- ^ <https://api.stackexchange.com/docs/types/reputation-history>
  | Revision -- ^ <https://api.stackexchange.com/docs/types/revision>
  | Site -- ^ <https://api.stackexchange.com/docs/types/site>
  | SuggestedEdit -- ^ <https://api.stackexchange.com/docs/types/suggested-edit>
  | Tag -- ^ <https://api.stackexchange.com/docs/types/tag>
  | TagScore -- ^ <https://api.stackexchange.com/docs/types/tag-score>
  | TagSynonym -- ^ <https://api.stackexchange.com/docs/types/tag-synonym>
  | TagWiki -- ^ <https://api.stackexchange.com/docs/types/tag-wiki>
  | TopTag -- ^ <https://api.stackexchange.com/docs/types/top-tag>
  | User -- ^ <https://api.stackexchange.com/docs/types/user>
  | UserTimeline -- ^ <https://api.stackexchange.com/docs/types/user-timeline>
  | WritePermission -- ^ <https://api.stackexchange.com/docs/types/write-permission>


-- | SE response value wrapper
newtype SE (a  Object) = SE { unSE  Value } deriving (Show, FromJSON)


-- | StackExchange API Request data type.
--
-- @a@ is a phantom type showing whether authentication is enabled
--
-- @n@ is a phantom type dissallowing combination of
-- different API calls in one request
--
-- @r@ is a type of parsed API call result
data R (a  Auth) (n  Symbol) r = R
  { _host  Text -- ^ API host link
  , _path  Text -- ^ API call link
  , _method  Text -- ^ API call method (GET/POST)
  , _query  Map Text Text -- ^ API call query parameters
  , _parse  Maybe (ByteString  r) -- ^ API call result parsing function
  }


makeLensesFor
  [ ("_host", "__host")
  , ("_path", "__path")
  , ("_method", "__method")
  , ("_query", "__query")
  , ("_parse", "__parse")
  ]
  ''R


type Request a n r = Dual (Endo (R a n r))


-- | Default StackExchange API request, defines only host link
instance Default (R a n r) where
  def = R
    { _host = "https://api.stackexchange.com/2.1"
    , _path = mempty
    , _method = "GET"
    , _query = mempty
    , _parse = Nothing
    }
  {-# INLINE def #-}


-- | Request defining only API call host
--
-- Primarily used in Auth, not intended for usage by library user
host  Text  Request a n r
host p = wrap $ __host .~ p
{-# INLINE host #-}


-- | Request defining only API call path
--
-- Primarily used in API call wrappers, not intended for usage by library user
path  Text  Request a n r
path p = wrap $ __path .~ p
{-# INLINE path #-}


-- | Request defining only call method
--
-- Primarily used in API call wrappers, not intended for usage by library user
method  Text  Request a n r
method m = wrap $ __method .~ m
{-# INLINE method #-}


-- | Request defining only API call result parsing function
--
-- Primarily used in API call wrappers, not intended for usage by library user
parse  (ByteString  r)  Request a n r
parse f = wrap $ __parse ?~ f
{-# INLINE parse #-}


-- | Request defining only API call query parameters
--
-- Rather low level interface. For more specific usage 'site',
-- 'filter', etc calls may be more convenient
--
--
-- Takes a list of (key, value) parameters such as @[(\"order\", \"asc\"), (\"sort\", \"rank\")]@
query  [(Text, Text)]  Request a n r
query q = wrap $ __query %~ (M.fromList q <>)
{-# INLINE query #-}


-- | Convert token requiring Request into ready one
token  Text  Request RequireToken n r  Request Ready n r
token t = unsafeCoerce . mappend (wrap (__query %~ M.insert "access_token" t))
{-# INLINE token #-}


-- | Request defining only App key
key  Text  Request a n r
key s = wrap $ __query %~ M.insert "key" s
{-# INLINE key #-}


-- | Request defining only API call site query parameter
site  Text  Request a n r
site s = wrap $ __query %~ M.insert "site" s
{-# INLINE site #-}


-- | Request defining only API call filter query parameter
filter  Text  Request a n r
filter f = wrap $ __query %~ M.insert "filter" f
{-# INLINE filter #-}


-- | Request defining only API call state query parameter
state  Text  Request a n r
state s = wrap $ __query %~ M.insert "state" s
{-# INLINE state #-}


-- | Scope defines permission granted for application by user
data Scope = ReadInbox | NoExpiry | WriteAccess | PrivateInfo


-- | Request defining only API call scope query parameter
scope  [Scope]  Request a n r
scope ss = wrap $ __query %~ (M.insert "scope" $ scopie ss)
 where
  scopie xs = T.intercalate "," . flip map xs $ \case
    ReadInbox    "read_inbox"
    NoExpiry     "no_expiry"
    WriteAccess  "write_access"
    PrivateInfo  "private_info"


-- | Request defining only Authentication API call application id
--
-- Primarily used in Authentication API call wrappers, not intended for usage by library user
client  Int  Request a n r
client (toLazyText . decimal  c) = wrap $ __query %~ M.insert "client_id" c
{-# INLINE client #-}


-- | Request defining only Authentication API call redirect url
--
-- Primarily used in Authentication API call wrappers, not intended for usage by library user
redirectURI  Text  Request a n r
redirectURI r = wrap $ __query %~ M.insert "redirect_uri" r
{-# INLINE redirectURI #-}


-- | Request defining only Authentication API call application secret
--
-- Primarily used in Authentication API call wrappers, not intended for usage by library user
secret  Text  Request a n r
secret c = wrap $ __query %~ M.insert "client_secret" c
{-# INLINE secret #-}


-- | Request defining only Authentication API call code
--
-- Primarily used in Authentication API call wrappers, not intended for usage by library user
code  Text  Request a n r
code c = wrap $ __query %~ M.insert "code" c
{-# INLINE code #-}


-- | Wrapping to interesting 'Monoid' ('R' -> 'R') instance
wrap  (R a n r  R a n r)  Request a n r
wrap = Dual . Endo


-- | Unwrapping from interesting 'Monoid' ('R' -> 'R') instance
unwrap  Request a n r  (R a n r  R a n r)
unwrap = appEndo . getDual


-- | Render 'R' as URI string for networking
render  Request a n r  String
render (($ def) . unwrap  R {_host, _path, _query}) =
  T.unpack $ mconcat [_host, "/", _path, "?", argie _query]
 where
  argie = T.intercalate "&" . M.foldrWithKey (\k v m  T.concat [k, "=", v] : m) mempty