{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE TemplateHaskell     #-}
{-# LANGUAGE RecordWildCards     #-}

{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE TypeFamilies        #-}
{-# LANGUAGE FlexibleInstances   #-}
{-# LANGUAGE TypeOperators       #-}
{-# LANGUAGE FlexibleContexts    #-}

module Avers.Server
    ( serveAversCoreAPI, serveAversSessionAPI

    , credentialsObjId

    , module Avers.Server.Authorization
    ) where


import           Control.Monad
import           Control.Monad.Except

import           Control.Concurrent
import           Control.Concurrent.STM

import           Data.Text              (Text)
-- import qualified Data.Text            as T
import qualified Data.Text.Encoding     as T

import qualified Data.ByteString.Lazy   as LBS
import qualified Data.ByteString.Base64 as BS64

import qualified Data.Set               as S


import           Data.Monoid
-- import Data.Proxy
-- import Data.Maybe
import           Data.Time
import           Data.Aeson (ToJSON, encode, decode)
-- import Data.Aeson.TH (defaultOptions)

import           Crypto.Hash.SHA256 (hashlazy)

import           Servant.API hiding (Patch)
import           Servant.Server

import           Avers
-- import Avers.TH
-- import Avers.Storage
-- import Avers.Storage.Expressions
-- import Avers.Types

import           Avers.API

import           Avers.Server.Authorization
import           Avers.Server.Instances ()

-- import qualified Database.RethinkDB as R

import           Network.HTTP.Types.Status

import           Network.Wai
import           Network.Wai.Handler.WebSockets (websocketsApp)
import qualified Network.WebSockets as WS

import           Web.Cookie

import           Prelude



etagVersion :: Text
etagVersion = "v0"


-- | Convert the 'Credentials' into an 'ObjId' to which the ceredentials refer.
-- That's the object the client is authenticated as.
credentialsObjId :: Handle -> Credentials -> ExceptT ServantErr IO ObjId
credentialsObjId aversH cred = do
    errOrObjId <- case cred of
        SessionIdCredential sId -> liftIO $ evalAvers aversH $
            sessionObjId <$> lookupSession sId

    case errOrObjId of
        Left _  -> throwError err401
        Right s -> pure s


failWith :: Text -> ExceptT ServantErr IO b
failWith e = throwError $ err500
    { errBody = LBS.fromChunks [T.encodeUtf8 e] }


aversResult :: Either AversError a -> ExceptT ServantErr IO a
aversResult res = case res of
    Left e -> case e of
        DatabaseError detail                  -> failWith $ "database " <> detail
        NotAuthorized                         -> failWith $ "Unauthorized"
        DocumentNotFound _                    -> failWith $ "NotFound"
        UnknownObjectType detail              -> failWith $ "unknown object " <> detail
        ObjectNotFound _                      -> failWith $ "NotFound"
        ParseError _ detail                   -> failWith $ "parse " <> detail
        PatchError (UnknownPatchError detail) -> failWith $ "patch " <> detail
        AversError detail                     -> failWith $ "avers " <> detail
        InternalError ie                      -> aversResult (Left ie)
    Right r -> pure r


reqAvers :: Handle -> Avers a -> ExceptT ServantErr IO a
reqAvers aversH m = liftIO (evalAvers aversH m) >>= aversResult


cacheableResponse :: (ToJSON a) => Maybe Text -> a -> ExceptT ServantErr IO (Cacheable a)
cacheableResponse mbValidationToken a = do
    let etag = T.decodeUtf8 $ BS64.encode $ hashlazy $ encode a
    if mbValidationToken == Just (etagVersion <> ":" <> etag)
        then throwError err304
        else pure $ addHeader "no-cache, public, max-age=63072000"
                  $ addHeader (etagVersion <> ":" <> etag) a


serveAversCoreAPI :: Handle -> Authorizations -> Server AversCoreAPI
serveAversCoreAPI aversH auth =
         serveCreateObject
    :<|> serveLookupObject
    :<|> servePatchObject
    :<|> serveDeleteObject
    :<|> serveLookupPatch
    :<|> serveObjectChanges
    :<|> serveCreateRelease
    :<|> serveLookupRelease
    :<|> serveLookupLatestRelease
    :<|> serveFeed
    :<|> serveChangeSecret

  where

    ----------------------------------------------------------------------------
    -- CreateObject
    serveCreateObject cred body = do
        let objType = cobType body

        runAuthorization aversH $
            createObjectAuthz auth cred objType


        createdBy <- credentialsObjId aversH cred
        objId <- reqAvers aversH $ do
            SomeObjectType ot <- lookupObjectType objType
            content <- case parseValueAs ot (cobContent body) of
                Left e  -> throwError e
                Right x -> pure x

            createObject ot createdBy content

        pure $ CreateObjectResponse objId (cobType body) (cobContent body)


    ----------------------------------------------------------------------------
    -- LookupObject
    serveLookupObject objId cred validationToken = do
        runAuthorization aversH $
            lookupObjectAuthz auth cred objId


        (Object{..}, Snapshot{..}) <- reqAvers aversH $ do
            object <- lookupObject objId
            snapshot <- lookupLatestSnapshot (BaseObjectId objId)

            pure (object, snapshot)

        cacheableResponse validationToken $ LookupObjectResponse
            { lorId = objId
            , lorType = objectType
            , lorCreatedAt = objectCreatedAt
            , lorCreatedBy = objectCreatedBy
            , lorRevisionId = snapshotRevisionId
            , lorContent = snapshotContent
            }

    ----------------------------------------------------------------------------
    -- PatchObject
    servePatchObject objId cred body = do
        -- runAuthorization aversH $
        --     lookupObjectAuthz auth cred objId


        authorObjId <- credentialsObjId aversH cred
        (previousPatches, numProcessedOperations, resultingPatches) <- reqAvers aversH $ do
            applyObjectUpdates
                (BaseObjectId objId)
                (pobRevisionId body)
                authorObjId
                (pobOperations body)
                False

        pure $ PatchObjectResponse
            { porPreviousPatches = previousPatches
            , porNumProcessedOperations = numProcessedOperations
            , porResultingPatches = resultingPatches
            }

    serveDeleteObject _ _ = throwError err500


    ----------------------------------------------------------------------------
    -- LookupPatch
    serveLookupPatch objId revId _cred validationToken = do
        -- TODO: authorization

        patch <- reqAvers aversH $ lookupPatch (BaseObjectId objId) revId
        cacheableResponse validationToken patch


    ----------------------------------------------------------------------------
    -- ObjectChanges
    serveObjectChanges objId _cred req respond = respond $
        case websocketsApp WS.defaultConnectionOptions wsApp req of
            Nothing  -> responseLBS status500 [] "Failed"
            Just res -> res
      where
        wsApp pendingConnection = do
            connection <- WS.acceptRequest pendingConnection
            WS.forkPingThread connection 10

            chan <- changeChannel aversH
            loop connection chan

        loop :: WS.Connection -> TChan Change -> IO ()
        loop connection chan = do
            change <- atomically $ readTChan chan
            case change of
                (CPatch patch) -> when (patchObjectId patch == BaseObjectId objId) $
                    WS.sendTextData connection (encode patch)

            loop connection chan



    serveCreateRelease _ _ _ = throwError err500
    serveLookupRelease _ _ _ _ = throwError err500
    serveLookupLatestRelease _ _ _ = throwError err500


    ----------------------------------------------------------------------------
    -- Feed
    serveFeed _cred req respond = respond $
        case websocketsApp WS.defaultConnectionOptions wsApp req of
            Nothing  -> responseLBS status500 [] "This is a WebSocket endpoint"
            Just res -> res
      where
        wsApp pendingConnection = do
            subscriptions <- newTVarIO S.empty
            connection <- WS.acceptRequest pendingConnection
            WS.forkPingThread connection 10

            void $ forkIO $ forever $ do
                msg <- WS.receiveData connection
                case decode msg of
                    Nothing -> pure ()
                    Just (IncludeObjectChanges objId) ->
                        atomically $ modifyTVar' subscriptions $ S.insert $ BaseObjectId objId

            chan <- changeChannel aversH
            loop connection subscriptions chan

        loop :: WS.Connection -> TVar (S.Set ObjectId) -> TChan Change -> IO ()
        loop connection subscriptions chan = do
            change <- atomically $ readTChan chan
            subs <- atomically $ readTVar subscriptions
            case change of
                (CPatch p) -> when (S.member (patchObjectId p) subs) $
                    WS.sendTextData connection (encode change)

            loop connection subscriptions chan


    serveChangeSecret _ _ = throwError err500




serveAversSessionAPI :: Handle -> Server AversSessionAPI
serveAversSessionAPI aversH =
         serveCreateSession
    :<|> serveLookupSession
    :<|> serveDeleteSession

  where

    sessionCookieName     = "session"
    sessionExpirationTime = 2 * 365 * 24 * 60 * 60

    mkSetCookie :: SessionId -> ExceptT ServantErr IO SetCookie
    mkSetCookie sId = do
        now <- liftIO $ getCurrentTime
        pure $ def
            { setCookieName = sessionCookieName
            , setCookieValue = T.encodeUtf8 (unSessionId sId)
            , setCookiePath = Just "/"
            , setCookieExpires = Just $ addUTCTime sessionExpirationTime now
            , setCookieHttpOnly = True
            }


    ----------------------------------------------------------------------------
    -- CreateSession
    serveCreateSession body = do
        -- Verify the secret, fail if it is invalid.
        reqAvers aversH $ verifySecret (csbLogin body) (csbSecret body)

        -- Create a new Session and save it in the database.
        now <- liftIO $ getCurrentTime
        sessId <- SessionId <$> liftIO (newId 80)
        -- isSecure <- rqIsSecure <$> getRequest

        let session = Session sessId (ObjId $ unSecretId $ csbLogin body) now now
        reqAvers aversH $ saveSession session

        setCookie <- mkSetCookie sessId
        pure $ addHeader setCookie $ CreateSessionResponse
            { csrSessionId = sessId
            , csrSessionObjId = ObjId $ unSecretId $ csbLogin body
            }


    ----------------------------------------------------------------------------
    -- LookupSession
    serveLookupSession sId = do
        session <- reqAvers aversH $ lookupSession sId

        setCookie <- mkSetCookie sId
        pure $ addHeader setCookie $ LookupSessionResponse
            { lsrSessionId = sessionId session
            , lsrSessionObjId = sessionObjId session
            }

    ----------------------------------------------------------------------------
    -- DeleteSession
    serveDeleteSession sId = do
        reqAvers aversH $ dropSession sId

        pure $ addHeader (def
            { setCookieName = sessionCookieName
            , setCookieExpires = Just $ UTCTime (ModifiedJulianDay 0) 0
            }) ()