{-# LANGUAGE ConstraintKinds       #-}
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE DefaultSignatures     #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE KindSignatures        #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeFamilies          #-}
module WebApi.Internal where

import           Blaze.ByteString.Builder (Builder, toByteString)
import qualified Blaze.ByteString.Builder.Char.Utf8 as Utf8 (fromText)
import           Control.Exception
import           Control.Monad.Catch (MonadCatch)
import           Control.Monad.IO.Class (MonadIO)
import           Control.Monad.Trans.Resource (runResourceT, withInternalState)
import           Data.ByteString (ByteString)
import           Data.ByteString.Char8 (pack, unpack)
import           Data.List (find, foldl')
import           Data.Monoid ((<>))
import           Data.Proxy
import           Data.Text (Text)
import qualified Data.Text as T (pack)
import           Data.Text.Encoding (decodeUtf8)
import           Data.Typeable (Typeable)
import qualified Network.HTTP.Client as HC
import           Network.HTTP.Media (MediaType, mapAcceptMedia, matchAccept)
import           Network.HTTP.Media.RenderHeader (renderHeader)
import           Network.HTTP.Types hiding (Query)
import           Network.URI (URI (..))
import qualified Network.Wai as Wai
import qualified Network.Wai.Parse as Wai
import           Web.Cookie
import           WebApi.ContentTypes
import           WebApi.Contract
import           WebApi.Param


data RouteResult a = NotMatched | Matched a

type RoutingApplication = Wai.Request -> (RouteResult Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived

toApplication :: RoutingApplication -> Wai.Application
toApplication app request respond =
  app request $ \routeResult -> case routeResult of
    Matched result -> respond result
    NotMatched -> respond (Wai.responseLBS notFound404 [] "")

fromWaiRequest :: ( FromParam (QueryParam m r) 'QueryParam
                   , FromParam (FormParam m r) 'FormParam
                   , FromParam (FileParam m r) 'FileParam
                   , FromHeader (HeaderIn m r)
                   , FromParam (CookieIn m r) 'Cookie
                   ) => Wai.Request
                 -> PathParam m r
                 -> IO (Validation [ParamErr] (Request m r))
fromWaiRequest waiReq pathPar = do
  (formPar, filePar) <- runResourceT $ withInternalState $
                        \internalState -> Wai.parseRequestBody (Wai.tempFileBackEnd internalState) waiReq
                          
  return $ Req <$> pure pathPar
    <*> (fromQueryParam $ Wai.queryString waiReq)
    <*> (fromFormParam formPar)
    <*> (fromFileParam filePar)
    <*> (fromHeader $ Wai.requestHeaders waiReq)
    <*> (fromCookie $ maybe [] parseCookies (getCookie waiReq))
    <*> (pure $ decodeUtf8 $ Wai.requestMethod waiReq)

toWaiResponse :: ( ToHeader (HeaderOut m r)
                  , ToParam (CookieOut m r) 'Cookie 
                  , Encodings (ContentTypes m r) (ApiOut m r)
                  , Encodings (ContentTypes m r) (ApiErr m r)
                  ) => Wai.Request -> Response m r -> Wai.Response
toWaiResponse wreq resp = case resp of
  Success status out hdrs cookies -> case encode' resp out of
    Just (ctype, o') -> let hds = (hContentType, renderHeader ctype) : handleHeaders' (toHeader hdrs) (toCookie cookies)
                        in Wai.responseBuilder status hds o'
    Nothing -> Wai.responseBuilder notAcceptable406 [] "Matching content type not found"
  Failure (Left (ApiError status errs hdrs cookies)) -> case encode' resp errs of
    Just (ctype, errs') -> let hds = (hContentType, renderHeader ctype) : handleHeaders (toHeader <$> hdrs) (toCookie <$> cookies)
                           in Wai.responseBuilder status hds errs'
    Nothing -> Wai.responseBuilder notAcceptable406 [] "Matching content type not found"
  Failure (Right (OtherError ex)) -> Wai.responseBuilder internalServerError500 [] (Utf8.fromText (T.pack (displayException ex)))

  where encode' :: ( Encodings (ContentTypes m r) a
                 ) => apiRes m r -> a -> Maybe (MediaType, Builder)
        encode' r o = case getAccept wreq of
          Just acc -> let ecs = encodings (reproxy r) o
                      in (,) <$> matchAccept (map fst ecs) acc <*> mapAcceptMedia ecs acc
          Nothing  -> case encodings (reproxy r) o of
            (x : _)  -> Just x
            _        -> Nothing

        reproxy :: apiRes m r -> Proxy (ContentTypes m r)
        reproxy = const Proxy

        handleHeaders :: Maybe [Header] -> Maybe [(ByteString, ByteString)] -> [Header]
        handleHeaders hds cks = handleHeaders' (maybe [] id hds) (maybe [] id cks)
 
        handleHeaders' :: [Header] -> [(ByteString, ByteString)] -> [Header]
        handleHeaders' hds cookies = let ckHs = map (\(ck, cv) -> (hSetCookie , renderSC ck cv)) cookies
                                     in hds <> ckHs
        renderSC k v = toByteString (renderSetCookie (def { setCookieName = k, setCookieValue = v }))

-- | Generate a type safe URL for a given route type. The URI can be used for setting a base URL if required.
link :: ( ToParam (QueryParam m r) 'QueryParam
        , MkPathFormatString r
        , ToParam (PathParam m r) 'PathParam
        ) =>
          route m r
        -> URI
        -> PathParam m r
        -> Maybe (QueryParam m r)
        -> URI
link r base paths query = base
                          { uriPath = unpack $ renderUriPath (pack $ uriPath base) paths r
                          , uriQuery = maybe "" renderQuery' query
                          }
  where renderQuery' :: (ToParam query 'QueryParam) => query -> String
        renderQuery' = unpack . renderQuery False . toQueryParam

renderUriPath ::  ( ToParam path 'PathParam
                   , MkPathFormatString r
                   ) => ByteString -> path -> route m r -> ByteString
renderUriPath basePth p r = case basePth of
          ""  -> renderPaths p r
          "/" -> renderPaths p r
          _   -> basePth `mappend` renderPaths p r

renderPaths :: ( ToParam path 'PathParam
                , MkPathFormatString r
                ) => path -> route m r -> ByteString
renderPaths p r = toByteString
                  $ encodePathSegments $ uriPathPieces (toPathParam p)

  where uriPathPieces :: [ByteString] -> [Text]
        uriPathPieces dynVs = reverse $ fst $ foldl' (flip fillHoles) ([], dynVs) (mkPathFormatString (toRoute r))

        fillHoles :: PathSegment -> ([Text], [ByteString]) -> ([Text], [ByteString])
        fillHoles (StaticSegment t) (segs, dynVs)    = (t : segs, dynVs)
        fillHoles  Hole             (segs, dynV: xs) = (decodeUtf8 dynV : segs, xs)
        fillHoles  Hole             (_segs, [])      = error "Panic: fewer pathparams than holes"

        toRoute :: (MkPathFormatString r) => route m r -> Proxy r
        toRoute = const Proxy

-- | Describes the implementation of a single API end point corresponding to @ApiContract (ApiInterface p) m r@
class (ApiContract (ApiInterface p) m r) => ApiHandler (p :: *) (m :: *) (r :: *) where
  -- | Handler for the API end point which returns a 'Response'.
  --
  -- TODO : 'query' type parameter is an experimental one used for trying out dependently typed params.
  -- This parameter will let us refine the 'ApiOut' to the structure that is requested by the client.
  -- for eg : graph.facebook.com/bgolub?fields=id,name,picture
  --
  -- This feature is not finalized and might get changed \/ removed.
  -- Currently the return type of handler is equivalent to `Response m r`
  --
  handler :: (query ~ '[])
            => Tagged query p
            -> Request m r
            -> HandlerM p (Query (Response m r) query)

type family Query (t :: *) (query :: [*]) :: * where
  Query t '[] = t

-- | Binds implementation to interface and provides a pluggable handler monad for the endpoint handler implementation.
class ( MonadCatch (HandlerM p)
      , MonadIO (HandlerM p)
      , WebApi (ApiInterface p)  
      ) => WebApiImplementation (p :: *) where
  -- | Type of the handler 'Monad'. It should implement 'MonadCatch' and 'MonadIO' classes. Defaults to 'IO'.
  type HandlerM p :: * -> *
  type ApiInterface p :: *     
  -- provides common defaulting information for api handlers

  -- | Create a value of @IO a@ from @HandlerM p a@.
  toIO :: p -> HandlerM p a -> IO a

  default toIO :: (HandlerM p ~ IO) => p -> HandlerM p a -> IO a
  toIO _ = id

  type HandlerM p = IO

-- | Type of settings of the server.
data ServerSettings = ServerSettings

-- | Default server settings.
serverSettings :: ServerSettings
serverSettings = ServerSettings

-- | Type of segments of a Path.
data PathSegment = StaticSegment Text -- ^ A static segment
                 | Hole -- ^ A dynamic segment
                 deriving Show

-- | Describe representation of the route.
class MkPathFormatString r where
  -- | Given a route, this function should produce the @[PathSegment]@ of that route. This gives the flexibility to hook in a different routing system into the application.
  mkPathFormatString :: Proxy r -> [PathSegment]

-- | Type of Exception raised in a handler.
data ApiException m r = ApiException { apiException :: ApiError m r }

instance Show (ApiException m r) where
  show (ApiException _) = "ApiException"

instance (Typeable m, Typeable r) => Exception (ApiException m r) where

handleApiException :: (query ~ '[], Monad (HandlerM p)) => p -> ApiException m r -> (HandlerM p) (Query (Response m r) query)
handleApiException _ = return . Failure . Left . apiException

handleSomeException :: (query ~ '[], Monad (HandlerM p)) => p -> SomeException -> (HandlerM p) (Query (Response m r) query)
handleSomeException _ = return . Failure . Right . OtherError 

getCookie :: Wai.Request -> Maybe ByteString
getCookie = fmap snd . find ((== hCookie) . fst) . Wai.requestHeaders

getAccept :: Wai.Request -> Maybe ByteString
getAccept = fmap snd . find ((== hAccept) . fst) . Wai.requestHeaders

hSetCookie :: HeaderName
hSetCookie = "Set-Cookie"

getContentType :: HC.Response a -> Maybe ByteString
getContentType = fmap snd . find ((== hContentType) . fst) . HC.responseHeaders

newtype Tagged (s :: [*]) b = Tagged { unTagged :: b } 

toTagged :: Proxy s -> b -> Tagged s b
toTagged _ = Tagged