{-# LANGUAGE CPP                   #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards       #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TemplateHaskell       #-}
{-# LANGUAGE TypeFamilies          #-}
module Application
    ( appMain
    , makeFoundation
    , makeLogWare
    -- * for DevelMain
    , getApplicationRepl
    , getAppSettings
    , shutdownAll
    , shutdownWeb
    , startWeb
    -- * for GHCI
    , handler
    , runDb
    , getAgentCtx
    , sleep
    )
where

import           Startlude               hiding (runReader)

import           Control.Concurrent.STM.TVar    ( newTVarIO )
import           Control.Monad.Logger
import           Control.Effect.Labelled        ( Labelled, runLabelled )
import qualified Data.HashMap.Strict           as HM
import           Data.IORef

import           Database.Persist.Sql
import           Database.Persist.Sqlite        ( createSqlitePool
                                                , runSqlite
                                                , sqlPoolSize
                                                , sqlDatabase
                                                )
import           Git.Embed
import           Network.HTTP.Client.TLS        ( getGlobalManager )
import           Network.Wai
import           Network.Wai.Handler.Warp       ( getPort )
import           System.Directory               ( createDirectoryIfMissing )
import           System.Environment             ( setEnv )
import           System.IO               hiding ( putStrLn, writeFile )
import           System.Log.FastLogger          ( defaultBufSize
                                                , newStdoutLoggerSet
                                                )
import           Yesod.Core
import           Yesod.Default.Config2
import           Yesod.Persist.Core

import           Constants
import qualified Daemon.AppNotifications       as AppNotifications
import           Daemon.RefreshProcDev
import qualified Daemon.SslRenew as SSLRenew
import           Daemon.TorHealth
import           Daemon.ZeroConf
import           Foundation
import           Lib.Algebra.State.RegistryUrl
import           Lib.Background
import           Lib.Database
import           Lib.External.Metrics.ProcDev
import           Lib.SelfUpdate
import           Lib.Sound
import           Lib.SystemPaths
import           Lib.Tor                        ( newTorManager )
import           Lib.WebServer
import           Model
import           Settings

appMain :: IO ()
appMain = do
    hSetBuffering stdout LineBuffering
    args <- getArgs

    -- Get the settings from all relevant sources
    settings <- loadYamlSettings [] [configSettingsYmlValue] useEnv

    settings' <- case args of
        ["--port", n] -> case readMaybe @Word16 $ toS n of
            Just n' -> pure $ settings { appPort = n' }
            Nothing -> do
                die . toS $ "Invalid Port: " <> n
        ["--git-hash"] -> do
            putStrLn @Text $embedGitRevision
            exitSuccess
        ["--version"] -> do
            putStrLn @Text (show agentVersion)
            exitSuccess
        _ -> pure settings
    createDirectoryIfMissing False (toS $ agentDataDirectory `relativeTo` appFilesystemBase settings')

    -- Generate the foundation from the settings
    foundation <- makeFoundation settings'

    startupSequence foundation

-- | This function allocates resources (such as a database connection pool),
-- performs initialization and returns a foundation datatype value. This is also
-- the place to put your migrate statements to have automatic database
-- migrations handled by Yesod.
makeFoundation :: AppSettings -> IO AgentCtx
makeFoundation appSettings = do
    now                         <- getCurrentTime
    -- Some basic initializations: HTTP connection manager, logger, and static
    -- subsite.
    appLogger                   <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger
    appHttpManager              <- getGlobalManager
    appTorManager               <- newTorManager (appTorSocksPort appSettings)
    appWebServerThreadId        <- newIORef Nothing
    appSelfUpdateSpecification  <- newEmptyMVar
    appIsUpdating               <- newIORef Nothing
    appIsUpdateFailed           <- newIORef Nothing
    appOsVersionLatest          <- newIORef Nothing
    appBackgroundJobs           <- newTVarIO (JobCache HM.empty)
    def                         <- getDefaultProcDevMetrics
    appProcDevMomentCache       <- newIORef (now, mempty, def)
    appLastTorRestart           <- newIORef now
    appLanThread                <- forkIO (sleep 10) >>= newMVar

    -- We need a log function to create a connection pool. We need a connection
    -- pool to create our foundation. And we need our foundation to get a
    -- logging function. To get out of this loop, we initially create a
    -- temporary foundation without a real connection pool, get a log function
    -- from there, and then create the real foundation.
    let mkFoundation appConnPool appIconTags = AgentCtx { .. }
    -- The AgentCtx {..} syntax is an example of record wild cards. For more
    -- information, see:
    -- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html
        tempFoundation = mkFoundation
            (panic "connPool forced in tempFoundation")
            (panic "iconTags forced in tempFoundation")
        logFunc        = messageLoggerSource tempFoundation appLogger

    db <- interpDb dbPath

    -- Create the database connection pool, will create sqlite file if doesn't already exist
    pool <- flip runLoggingT logFunc $ createSqlitePool (toS db) (sqlPoolSize . appDatabaseConf $ appSettings)

    -- run migrations only if agent in charge
    when (appPort appSettings == 5959) $ do
        runSqlite db $ runMigration migrateAll
        void . interpDb $ ensureCoherentDbVersion pool logFunc

    iconTags <- if appPort appSettings == 5959
        then do
            iconDigests <- runSqlPool (selectList [] []) pool
            newTVarIO . HM.fromList $ (unIconDigestKey . entityKey &&& iconDigestTag . entityVal) <$> iconDigests
        else newTVarIO HM.empty

    -- Return the foundation
    pure $ mkFoundation pool iconTags
    where
        interpDb :: (Labelled "sqlDatabase" (ReaderT Text)) (Labelled "filesystemBase" (ReaderT Text) IO) a -> IO a
        interpDb = injectFilesystemBaseFromContext appSettings
                . flip runReaderT (sqlDatabase . appDatabaseConf $ appSettings)
                . runLabelled @"sqlDatabase"

getAppSettings :: IO AppSettings
getAppSettings = loadYamlSettings [configSettingsYml] [] useEnv


startupSequence :: AgentCtx -> IO ()
startupSequence foundation = do

#ifdef DISABLE_AUTH
    withAgentVersionLog_ "[WARNING] Agent auth disabled!"
#endif

    injectFilesystemBaseFromContext (appSettings foundation) . runRegistryUrlIOC $ getRegistryUrl >>= \case
        Nothing -> pure ()
        Just x -> liftIO $ do
            withAgentVersionLog "Detected Alternate Registry URL" x
            -- this is so that appmgr inherits the alternate registry url when it is called.
            setEnv "REGISTRY_URL" (show x)

    -- proc dev metrics refresh loop
    withAgentVersionLog_ "Initializing proc dev refresh loop"
    void . forkIO . forever $ forkIO (refreshProcDev foundation) >> threadDelay 5_000_000
    withAgentVersionLog_ "Proc dev metrics refreshing"

    -- web
    withAgentVersionLog_ "Starting web server"
    void . forkIO . startWeb $ foundation
    withAgentVersionLog_ "Web server running"

    -- all these actions are destructive in some way, and only webserver is needed for self-update
    when (appPort (appSettings foundation) == 5959) $ do
        synchronizeSystemState foundation agentVersion

        -- app notifications refresh loop
        withAgentVersionLog_ "Initializing app notifications refresh loop"
        void . forkIO . forever $ forkIO (runReaderT AppNotifications.fetchAndSave foundation) >> threadDelay 5_000_000
        withAgentVersionLog_ "App notifications refreshing"

        withAgentVersionLog_ "Initializing SSL certificate renewal loop"
        void . forkIO . forever $ forkIO (SSLRenew.renewSslLeafCert foundation) *> sleep 86_400
        withAgentVersionLog_ "SSL Renewal daemon started"

        withAgentVersionLog_ "Initializing Tor health check loop"
        void . forkIO . forever $ forkIO (runReaderT torHealth foundation) *> sleep 300
        withAgentVersionLog_ "Tor health check loop running"

        -- reloading avahi daemon
        -- DRAGONS! make sure this step happens AFTER system synchronization
        withAgentVersionLog_ "Publishing Agent to Avahi Daemon"
        runReaderT publishAgentToAvahi foundation
        withAgentVersionLog_ "Avahi Daemon reloaded with Agent service"

        when (appPort (appSettings foundation) == 5959) $ do
            playSong 400 marioCoin

    withAgentVersionLog_ "Listening for Self-Update Signal"
    waitForUpdateSignal foundation

sleep :: Integer -> IO ()
sleep n = let (full, r) = (n * 1_000_000) `divMod` fromIntegral (maxBound :: Int) in
    replicateM_ (fromIntegral full) (threadDelay maxBound) *> threadDelay (fromIntegral r)

--------------------------------------------------------------
-- Functions for DevelMain.hs (a way to run the AgentCtx from GHCi)
--------------------------------------------------------------

getApplicationRepl :: IO (Int, AgentCtx, Application)
getApplicationRepl = do
    foundation <- getAppSettings >>= makeFoundation
    wsettings  <- getDevSettings $ warpSettings foundation
    app1       <- makeApplication foundation
    return (getPort wsettings, foundation, app1)

getAgentCtx :: IO AgentCtx
getAgentCtx = getAppSettings >>= makeFoundation

---------------------------------------------
-- Functions for use in development with GHCi
---------------------------------------------

-- | Run a handler
handler :: Handler a -> IO a
handler h = getAppSettings >>= makeFoundation >>= flip unsafeHandler h

-- | Run DB queries
runDb :: ReaderT SqlBackend Handler a -> IO a
runDb = handler . runDB

