{-# LANGUAGE AllowAmbiguousTypes #-}

module Jobs.HealthReport
  ( generateHealthReport
  ) where

import Import

import Data.List (genericLength)
import qualified Data.Map.Strict as Map

import qualified Data.Aeson as Aeson

import qualified Data.ByteArray as ByteArray

import Network.HTTP.Simple (httpJSON, httpLBS)
import qualified Network.HTTP.Simple as HTTP

import qualified Database.Esqueleto.Legacy as E

import Auth.LDAP

import qualified Data.CaseInsensitive as CI

import qualified Network.HaskellNet.SMTP as SMTP
import UnliftIO.Pool (withResource)

import Jobs.Queue

import UnliftIO.Concurrent (myThreadId)


generateHealthReport :: HealthCheck -> Handler HealthReport
generateHealthReport = withHealthReportMetrics . $(dispatchTH ''HealthCheck)


dispatchHealthCheckMatchingClusterConfig :: Handler HealthReport
-- ^ Can the cluster configuration be read from the database and does it match our configuration?
dispatchHealthCheckMatchingClusterConfig
  = fmap HealthMatchingClusterConfig . yesodTimeout (^. _appHealthCheckMatchingClusterConfigTimeout) False . runDB $ and <$> forM universeF clusterSettingMatches
  where
    clusterSettingMatches ClusterCryptoIDKey = do
      ourSetting <- getsYesod appCryptoIDKey
      dbSetting <- clusterSetting @'ClusterCryptoIDKey
      return $ ((==) `on` fmap (ByteArray.convert :: CryptoIDKey -> ByteString)) (Just ourSetting) dbSetting
    clusterSettingMatches ClusterServerSessionKey = do
      ourSetting <- getsYesod . preview $ _appSessionStore . _SessionStorageMemcachedSql . _mcdSqlMemcachedKey
      dbSetting <- clusterSetting @'ClusterServerSessionKey
      return $ maybe True ((== dbSetting) . Just) ourSetting
    clusterSettingMatches ClusterSecretBoxKey = do
      ourSetting <- getsYesod appSecretBoxKey
      dbSetting <- clusterSetting @'ClusterSecretBoxKey
      return $ Just ourSetting == dbSetting
    clusterSettingMatches ClusterJSONWebKeySet = do
      ourSetting <- getsYesod appJSONWebKeySet
      dbSetting <- clusterSetting @'ClusterJSONWebKeySet
      return $ Just ourSetting == dbSetting
    clusterSettingMatches ClusterId = do
      ourSetting <- getsYesod appClusterID
      dbSetting <- clusterSetting @'ClusterId
      return $ Just ourSetting == dbSetting
    clusterSettingMatches ClusterMemcachedKey = do
      ourSetting <- getsYesod $ fmap memcachedKey . appMemcached
      dbSetting <- clusterSetting @'ClusterMemcachedKey
      return $ maybe True ((== dbSetting) . Just) ourSetting
    clusterSettingMatches ClusterVerpSecret = do
      ourSetting <- getsYesod appVerpSecret
      dbSetting <- clusterSetting @'ClusterVerpSecret
      return $ Just ourSetting == dbSetting
    clusterSettingMatches ClusterAuthKey = do
      ourSetting <- getsYesod appAuthKey
      dbSetting <- clusterSetting @'ClusterAuthKey
      return $ Just ourSetting == dbSetting
    clusterSettingMatches ClusterPersonalisedSheetFilesSeedKey = do
      ourSetting <- getsYesod appPersonalisedSheetFilesSeedKey
      dbSetting <- clusterSetting @'ClusterPersonalisedSheetFilesSeedKey
      return $ Just ourSetting == dbSetting


    clusterSetting :: forall key.
                      ( ClusterSetting key
                      )
                   => DB (Maybe (ClusterSettingValue key))
    clusterSetting = do
      current' <- get . ClusterConfigKey $ knownClusterSetting (Proxy @key)
      case Aeson.fromJSON . clusterConfigValue <$> current' of
        Just (Aeson.Success c) -> return $ Just c
        _other                 -> return Nothing


dispatchHealthCheckHTTPReachable :: Handler HealthReport
dispatchHealthCheckHTTPReachable = fmap HealthHTTPReachable . yesodTimeout (^. _appHealthCheckHTTPReachableTimeout) (Just False) $ do
  staticAppRoot <- getsYesod $ views _appRoot ($ ApprootDefault)
  doHTTP <- getsYesod $ view _appHealthCheckHTTP
  for (staticAppRoot <* guard doHTTP) $ \_ -> do
    url <- getUrlRender <*> pure InstanceR
    baseRequest <- HTTP.parseRequest $ unpack url
    httpManager' <- getsYesod appHttpManager
    let httpRequest = baseRequest
          & HTTP.setRequestManager httpManager'
    (clusterId, _ :: InstanceId) <- responseBody <$> httpJSON httpRequest
    getsYesod $ (== clusterId) . appClusterID


dispatchHealthCheckLDAPAdmins :: Handler HealthReport
dispatchHealthCheckLDAPAdmins = fmap HealthLDAPAdmins . yesodTimeout (^. _appHealthCheckLDAPAdminsTimeout) (Just 0) $ do
  ldapPool' <- getsYesod appLdapPool
  reTestAfter <- getsYesod $ view _appLdapReTestFailover
  case ldapPool' of
    Just ldapPool -> do
      ldapAdminUsers' <- fmap (map E.unValue) . runDB . E.select . E.from $ \(user `E.InnerJoin` userFunction) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId] $ do
        E.on $ user E.^. UserId E.==. userFunction E.^. UserFunctionUser
        E.where_ $ userFunction E.^. UserFunctionFunction E.==. E.val SchoolAdmin
        E.where_ $ user E.^. UserAuthentication E.==. E.val AuthLDAP
        return $ user E.^. UserIdent
      for (assertM' (not . null) ldapAdminUsers') $ \ldapAdminUsers -> do
        let numAdmins = genericLength ldapAdminUsers
        Sum numResolved <- fmap fold . forM ldapAdminUsers $ \(CI.original -> adminIdent) ->
          let hCampusExc :: CampusUserException -> Handler (Sum Integer)
              hCampusExc err = mempty <$ $logErrorS "healthCheckLDAPAdmins" (adminIdent <> ": " <> tshow err)
           in handle hCampusExc $ Sum 1 <$ campusUserReTest ldapPool ((>= reTestAfter) . realToFrac) FailoverUnlimited (Creds apLdap adminIdent [])
        return $ numResolved % numAdmins
    _other -> return Nothing


dispatchHealthCheckSMTPConnect :: Handler HealthReport
dispatchHealthCheckSMTPConnect = fmap HealthSMTPConnect . yesodTimeout (^. _appHealthCheckSMTPConnectTimeout) (Just False) $ do
  smtpPool <- getsYesod appSmtpPool
  for smtpPool . flip withResource $ \smtpConn -> do
    response@(rCode, _) <- liftIO $ SMTP.sendCommand smtpConn SMTP.NOOP
    case rCode of
      250 -> return True
      _   -> do
        $logErrorS "Mail" $ "NOOP failed: " <> tshow response
        return False


dispatchHealthCheckWidgetMemcached :: Handler HealthReport
dispatchHealthCheckWidgetMemcached = fmap HealthWidgetMemcached . yesodTimeout (^. _appHealthCheckActiveWidgetMemcachedTimeout) (Just False) $ do
  memcachedConn <- getsYesod appWidgetMemcached
  for memcachedConn $ \_memcachedConn' -> do
    let ext :: Text
        ext = "bin"
        mimeType :: Text
        mimeType = "application/octet-stream"
    content <- pack . take 256 <$> liftIO getRandoms
    staticLink <- addStaticContent ext mimeType content
    doHTTP <- getsYesod $ view _appHealthCheckHTTP
    case staticLink of
      _ | not doHTTP -> return True
      Just (Left url) -> do
        baseRequest <- HTTP.parseRequest $ unpack url
        httpManager' <- getsYesod appHttpManager
        let httpRequest = baseRequest
              & HTTP.setRequestManager httpManager'
        (== content) . responseBody <$> httpLBS httpRequest
      _other -> return False


dispatchHealthCheckActiveJobExecutors :: Handler HealthReport
dispatchHealthCheckActiveJobExecutors = HealthActiveJobExecutors <$> do
  app <- getYesod
  jState <- atomically . tryReadTMVar $ appJobState app
  let configuredNumber = app ^. _appJobWorkers
      timeoutLength = app ^. _appHealthCheckActiveJobExecutorsTimeout
  case jState of
    Nothing
      | configuredNumber == 0 -> return Nothing
    Nothing -> return $ Just 0
    Just JobState{jobWorkers, jobWorkerName} -> do
      tid <- myThreadId
      let workers' = Map.fromSet jobWorkerName (Map.keysSet jobWorkers)
          workers = Map.filterWithKey (\a _ -> asyncThreadId a /= tid) workers'
      $logDebugS "HealthCheckActiveJobExecutors" . tshow . map showWorkerId $ Map.elems workers'
      responders <- fmap (getSum . fold) . liftIO . forConcurrently (Map.toList workers) $ \(_, wName)
        -> diffTimeout timeoutLength (Sum 0) (runReaderT ?? app $ Sum 1 <$ writeJobCtlBlock' (writeJobCtl' wName) JobCtlTest)
      if
        | Map.null workers -> return Nothing
        | otherwise        -> return . Just $ responders % fromIntegral (Map.size workers)


dispatchHealthCheckDoesFlush :: Handler HealthReport
dispatchHealthCheckDoesFlush = fmap HealthDoesFlush . runMaybeT $ do
  UniWorX{ appSettings' = AppSettings{..}, appJobState } <- getYesod

  guard . is _Just $ guardOnM (isn't _JobsOffload appJobMode) appJobFlushInterval
  checkInterval <- hoistMaybe $ appHealthCheckInterval HealthCheckDoesFlush
  lFlush <- MaybeT . atomically $ do
    jState <- tryReadTMVar appJobState
    fmap join . for jState $ \JobState{jobContext} -> readTVar $ jobLastFlush jobContext

  now <- liftIO getCurrentTime
  return $ toRational (now `diffUTCTime` lFlush) / toRational checkInterval
