{-# LANGUAGE CPP                   #-}
{-# LANGUAGE InstanceSigs          #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes           #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE RankNTypes            #-}
{-# LANGUAGE TemplateHaskell       #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE ViewPatterns          #-}

module Foundation where

import           Startlude

import qualified Control.Effect.Labelled       as FE
import qualified Control.Carrier.Lift          as FE
import           Control.Concurrent.STM
import           Control.Monad.Base
import           Control.Monad.Logger           ( LogSource )
import           Control.Monad.Trans.Control
import           Crypto.Hash                    ( MD5, Digest )
import qualified Data.HashMap.Strict           as HM
import           Data.IORef
import           Data.Set
import           Data.UUID
import           Database.Persist              as Persist
import           Database.Persist.Sql
import           Network.HTTP.Client            (Manager)
import           Network.HTTP.Types             (status200)
import           Network.Wai
import           Yesod.Core
import           Yesod.Core.Types
import           Yesod.Auth                     ( AuthenticationResult(..)
                                                , Creds(..)
                                                , YesodAuth(..)
                                                , YesodAuthPersist
                                                , maybeAuth
                                                )
import qualified Yesod.Auth.Message            as Msg
import           Yesod.Form
import qualified Yesod.Core.Unsafe             as Unsafe
import           Yesod.Persist.Core

import           Auth
import           Constants
import           Lib.Algebra.State.RegistryUrl
import           Lib.Background
import           Lib.Error
import           Lib.External.Metrics.ProcDev
import           Lib.SystemPaths
import           Lib.Types.Core
import           Lib.Types.Emver
import           Model
import           Settings


-- | The foundation datatype for your application. This can be a good place to
-- keep settings and values requiring initialization before your application
-- starts running, such as database connections. Every handler will have
-- access to the data present here.
data OsVersionCache = OsVersionCache { osVersion :: Version, lastChecked :: UTCTime }

data AgentCtx = AgentCtx
    { appSettings                 :: AppSettings
    , appHttpManager              :: Manager
    , appTorManager               :: Manager
    , appConnPool                 :: ConnectionPool -- ^ Database connection pool.
    , appLogger                   :: Logger
    , appWebServerThreadId        :: IORef (Maybe ThreadId)
    , appIsUpdating               :: IORef (Maybe Version)
    , appIsUpdateFailed           :: IORef (Maybe S9Error)
    , appOsVersionLatest          :: IORef (Maybe OsVersionCache)
    , appProcDevMomentCache       :: IORef (UTCTime, ProcDevMomentStats, ProcDevMetrics)
    , appSelfUpdateSpecification  :: MVar VersionRange
    , appBackgroundJobs           :: TVar JobCache
    , appIconTags                 :: TVar (HM.HashMap AppId (Digest MD5))
    , appLastTorRestart           :: IORef UTCTime
    , appLanThread                :: MVar ThreadId
    }

setWebProcessThreadId :: ThreadId -> AgentCtx -> IO ()
setWebProcessThreadId tid a = writeIORef (appWebServerThreadId a) . Just $ tid

-- This is where we define all of the routes in our application. For a full
-- explanation of the syntax, please see:
-- http://www.yesodweb.com/book/routing-and-handlers
--
-- Note that this is really half the story; in Application.hs, mkYesodDispatch
-- generates the rest of the code. Please see the following documentation
-- for an explanation for this split:
-- http://www.yesodweb.com/book/scaffolding-and-the-site-template#scaffolding-and-the-site-template_foundation_and_application_modules
--
-- This function also generates the following type synonyms:
-- type Handler = HandlerT AgentCtx IO
mkYesodData "AgentCtx" $(parseRoutesFile "config/routes")

noCacheUnlessSpecified :: Handler a -> Handler a
noCacheUnlessSpecified action = do
    getCurrentRoute >>= \case
        Nothing -> action
        Just r -> if "cached" `member` routeAttrs r
            then action
            else addHeader "Cache-Control" "no-store" >> action
-- Please see the documentation for the Yesod typeclass. There are a number
-- of settings which can be configured by overriding methods here.
instance Yesod AgentCtx where
    approot = ApprootRelative
    authRoute _ = Nothing

    isAuthorized route _ | "noAuth" `member` routeAttrs route = pure Authorized
                         -- HACK! So that updating from 0.1.5 to 0.2.x doesn't leave you unreachable during system sync
                         -- in the old companion
                         | (fst $ renderRoute route) == ["v0"] = do
                             isUpdating <- fmap isJust $ getsYesod appIsUpdating >>= liftIO . readIORef
                             fresh <- fmap Startlude.null . runDB $ selectList ([] :: [Filter Account]) []
                             if isUpdating && fresh
                                 then sendResponseStatus status200 (object ["status" .= ("UPDATING" :: Text)])
                                 else requireSessionAuth
                         | otherwise                          = requireSessionAuth

-- Yesod Middleware allows you to run code before and after each handler function.
-- The defaultYesodMiddleware adds the response header "Vary: Accept, Accept-Language" and performs authorization checks.
-- Some users may also want to add the defaultCsrfMiddleware, which:
--   a) Sets a cookie with a CSRF token in it.
--   b) Validates that incoming write requests include that token in either a header or POST parameter.
-- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware
-- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package.
    yesodMiddleware :: ToTypedContent res => Handler res -> Handler res
    yesodMiddleware = defaultYesodMiddleware . cutoffDuringUpdate . noCacheUnlessSpecified

-- What messages should be logged. The following includes all messages when
-- in development, and warnings and errors in production.
    shouldLogIO :: AgentCtx -> LogSource -> LogLevel -> IO Bool
    shouldLogIO app _source level =
        return $ appShouldLogAll (appSettings app) || level == LevelInfo || level == LevelWarn || level == LevelError

    makeLogger :: AgentCtx -> IO Logger
    makeLogger = return . appLogger

    makeSessionBackend :: AgentCtx -> IO (Maybe SessionBackend)
    makeSessionBackend ctx = strictSameSiteSessions $ do
        filepath <- injectFilesystemBaseFromContext settings $ getAbsoluteLocationFor sessionSigningKeyPath
        fmap Just $ defaultClientSessionBackend minutes $ toS filepath
        where
            settings = appSettings ctx
            minutes = 7 * 24 * 60 -- 7 days

instance RenderMessage AgentCtx FormMessage where
    renderMessage _ _ = defaultFormMessage
instance YesodAuth AgentCtx where
    type AuthId AgentCtx = AccountId
    loginDest _ = AuthenticateR
    logoutDest _ = AuthenticateR
    authPlugins _ = []

    -- This gets called on login, but after HashDB's postLoginR handler is called.  This validates the username and password, so creds here are legit.
    authenticate creds = liftHandler $ runDB $ do
        x <- getBy $ UniqueAccount $ credsIdent creds
        pure $ case x of
            Just (Entity uid _) -> Authenticated uid
            Nothing -> UserError Msg.NoIdentifierProvided

instance YesodAuthPersist AgentCtx

-- How to run database actions.
instance YesodPersist AgentCtx where
    type YesodPersistBackend AgentCtx = SqlBackend
    runDB :: SqlPersistT Handler a -> Handler a
    runDB action = runSqlPool action . appConnPool =<< getYesod

instance YesodPersistRunner AgentCtx where
    getDBRunner :: Handler (DBRunner AgentCtx, Handler ())
    getDBRunner = defaultGetDBRunner appConnPool

unsafeHandler :: AgentCtx -> Handler a -> IO a
unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger

appLogFunc :: AgentCtx -> LogFunc
appLogFunc = appLogger >>= flip messageLoggerSource

cutoffDuringUpdate :: Handler a -> Handler a
cutoffDuringUpdate m = do
    appIsUpdating <- getsYesod appIsUpdating >>= liftIO . readIORef
    case appIsUpdating of
        Just _ -> do
            path <- asks $ pathInfo . reqWaiRequest . handlerRequest
            case path of
                [v] | v == "v" <> (show . major $ agentVersion) -> m
                [auth] | auth == "auth" -> m
                (_:ssh:_) | ssh == "sshKeys" -> m
                _ -> handleS9ErrT $ throwE UpdateInProgressE
        Nothing -> m

-- Returns authorized iff there is a valid (non-expired, signed + encrypted) session containing an account.
-- The only way for such a session to exist is if a previous login succeeded
requireSessionAuth :: Handler AuthResult
requireSessionAuth = do
#ifdef DISABLE_AUTH
    pure Authorized
#else
    maybeAuth >>= \case
        Nothing -> pure AuthenticationRequired
        Just _  -> pure Authorized
#endif

type AgentRunner m =
    RegistryUrlIOC (FE.Labelled "filesystemBase" (ReaderT Text) (FE.Labelled "httpManager" (ReaderT Manager) (FE.LiftC (ReaderT AgentCtx m))))

runInContext :: MonadResource m => AgentRunner m a -> ReaderT AgentCtx m a
runInContext action = do
    ctx <- ask
    let s = appSettings ctx
    action
        & runRegistryUrlIOC
        & FE.runLabelled @"filesystemBase"
        & flip runReaderT (appFilesystemBase s)
        & FE.runLabelled @"httpManager"
        & flip runReaderT (appHttpManager ctx)
        & FE.runM

instance MonadBase IO Handler where
    liftBase m = HandlerFor $ const m
instance MonadBaseControl IO Handler where
    type StM Handler a = a
    liftBaseWith f = HandlerFor $ \handlerData -> f (($ handlerData) . unHandlerFor)
    restoreM = pure
