{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns        #-}
{-# LANGUAGE NoImplicitPrelude     #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE RankNTypes            #-}

{-| This module contains:

* Functions exported on the public API
* High level message handlers of the supervisor thread loop

-}
module Control.Concurrent.Capataz.Internal.Core
(
  HasSupervisor(..)
, forkWorker
, forkSupervisor
, forkCapataz
, terminateProcess
, terminateCapataz
, terminateCapataz_
, joinCapatazThread
, getSupervisorProcessId
, getSupervisorAsync
, getCapatazTeardown
)
where

import RIO

import Control.Teardown (Teardown, TeardownResult, newTeardown, runTeardown, runTeardown_)
import RIO.Time         (getCurrentTime)

import qualified Data.UUID.V4 as UUID (nextRandom)

import qualified Control.Concurrent.Capataz.Internal.Supervisor as Supervisor

import           Control.Concurrent.Capataz.Internal.Types
import qualified Control.Concurrent.Capataz.Internal.Util  as Util

--------------------------------------------------------------------------------

-- | Utility typeclass to call public supervision API with types
-- that contain a supervisor (e.g. Capataz record).
class HasSupervisor a where
  -- | Fetches a supervisor from a record internals.
  getSupervisor :: a m -> Supervisor m

instance HasSupervisor Capataz where
  getSupervisor Capataz {capatazSupervisor} = capatazSupervisor

instance HasSupervisor Supervisor where
  getSupervisor = id

-- | Creates a Capataz record, which holds both a root supervisor and a
-- 'Teardown' to shut down the system. The root supervisor monitors failures on
-- process threads defined with 'supervisorProcessSpecList' or created
-- dynamically using 'forkWorker' or 'forkSupervisor'.
forkCapataz
  :: (MonadUnliftIO m, MonadIO m)
  => Text
  -> (CapatazOptions m -> CapatazOptions m)
  -> m (Capataz m)
forkCapataz capatazName modOptionsFn = do
  capatazId    <- liftIO UUID.nextRandom
  supervisorId <- liftIO UUID.nextRandom
  let
    capatazOptions@CapatazOptions { notifyEvent } =
      defCapatazOptions capatazName modOptionsFn
    supervisorOptions@SupervisorOptions { supervisorName } =
      Util.capatazOptionsToSupervisorOptions capatazOptions
    parentSupervisorEnv = ParentSupervisorEnv
      { supervisorId     = capatazId
      , supervisorName   = "capataz-root"
      , supervisorNotify = \supervisorEvent -> do
        eventTime <- getCurrentTime
        case supervisorEvent of
          MonitorEvent ProcessFailed' { processError } -> notifyEvent
            CapatazFailed
              { supervisorId
              , supervisorName
              , eventTime
              , supervisorError = processError
              }

          MonitorEvent ProcessTerminated'{} -> notifyEvent CapatazTerminated
            { supervisorId
            , supervisorName
            , eventTime
            }

          MonitorEvent ProcessCompleted'{} ->
            error "Capataz completed; this should never happen"

          MonitorEvent ProcessForcedRestart{} ->
            error
              "Capataz was restarted from a OneForAll strategy; this should never happen"

          ControlAction{} ->
            error "Capataz received a ControlAction message; bad implementation"
      , notifyEvent
      }

  capatazSupervisor@Supervisor { supervisorEnv } <- Supervisor.supervisorMain
    parentSupervisorEnv
    supervisorOptions
    supervisorId
    0 -- initial restart count

  capatazTeardown <- withRunInIO $ \run -> newTeardown
    "capataz"
    (run $ do
      Supervisor.haltSupervisor "capataz system shutdown" supervisorEnv
      eventTime <- getCurrentTime
      notifyEvent CapatazTerminated {supervisorId , supervisorName , eventTime }
    )

  return Capataz {capatazSupervisor , capatazTeardown }

-- | Creates a green thread from an @IO ()@ sub-routine. Depending in options
-- defined in the 'WorkerOptions' record, it will automatically restart this
-- sub-routine in case of failures.
--
-- See documentation of related functions:
--
-- * 'buildWorkerOptionsWithDefaults'
-- * 'buildWorkerOptions'
--
forkWorker
  :: (MonadIO m, HasSupervisor supervisor)
  => WorkerOptions m -- ^ Worker options (restart, name, callbacks, etc)
  -> supervisor m   -- ^ 'Supervisor' that supervises the worker
  -> m WorkerId   -- ^ An identifier that can be used to terminate the 'Worker'
forkWorker workerOptions sup = do
  let Supervisor { supervisorNotify } = getSupervisor sup
  workerIdVar <- newEmptyMVar
  supervisorNotify
    (ControlAction ForkWorker
      { workerOptions
      , returnWorkerId = putMVar workerIdVar
      }
    )
  takeMVar workerIdVar

-- | Creates a green thread which monitors other green threads for failures and
-- restarts them using settings defined on 'SupervisorOptions'.
--
-- See documentation of related functions:
--
-- * 'buildSupervisorOptionsWithDefault'
-- * 'buildSupervisorOptions'
--
forkSupervisor
  :: (MonadIO m, HasSupervisor parentSupervisor)
  => SupervisorOptions m -- ^ Supervisor options
  -> parentSupervisor m  -- ^ Parent supervisor instance that supervises new supervisor
  -> m (Supervisor m)     -- ^ A record used to dynamically create and supervise
                       -- other processes
forkSupervisor supervisorOptions parentSup = do
  let Supervisor { supervisorNotify } = getSupervisor parentSup
  supervisorVar <- newEmptyMVar
  supervisorNotify
    (ControlAction ForkSupervisor
      { supervisorOptions
      , returnSupervisor  = putMVar supervisorVar
      }
    )
  takeMVar supervisorVar

-- | Stops the execution of a green thread being supervised by the given
-- supervisor.
--
-- __IMPORTANT__ If 'ProcessId' maps to a worker that is configured with a
-- 'Permanent' worker restart strategy, the worker green thread __will be
-- restarted again__.
--
terminateProcess
  :: (MonadIO m, HasSupervisor supervisor)
  => Text
  -> ProcessId
  -> supervisor m
  -> m Bool
terminateProcess processTerminationReason processId supervisor = do
  let Supervisor { supervisorNotify } = getSupervisor supervisor
  result <- newEmptyMVar
  supervisorNotify
    (ControlAction TerminateProcess
      { processId
      , processTerminationReason
      , notifyProcessTermination = putMVar result
      }
    )
  takeMVar result

-- | Joins the thread of the root supervisor of the given capataz system to the
-- current thread.
joinCapatazThread :: MonadIO m => Capataz m -> m ()
joinCapatazThread Capataz { capatazSupervisor } =
  let Supervisor { supervisorAsync } = capatazSupervisor
  in  wait supervisorAsync

-- | Terminates a 'Capataz' system (all supervised threads) and returns a 'TeardownResult'
--
-- @since 0.2.0.0
terminateCapataz :: MonadIO m => Capataz m -> m TeardownResult
terminateCapataz = liftIO . runTeardown

-- | Terminates a 'Capataz' system (all supervised threads)
--
-- @since 0.2.0.0
terminateCapataz_ :: MonadIO m => Capataz m -> m ()
terminateCapataz_ = liftIO . runTeardown_

-- | Gets 'Teardown' record of this capataz system.
getCapatazTeardown :: Capataz m -> Teardown
getCapatazTeardown Capataz { capatazTeardown } = capatazTeardown

-- | Gets the 'Async' of a Supervisor thread.
--
-- NOTE: There is no way to get the 'Async' value of the root supervisor; this
-- is done on-purpose to avoid error scenarios.
getSupervisorAsync :: Supervisor m -> Async ()
getSupervisorAsync Supervisor { supervisorAsync } = supervisorAsync

-- | Gets the process identifier of a 'Supervisor'; normally used for termination.
getSupervisorProcessId :: Supervisor m -> ProcessId
getSupervisorProcessId Supervisor { supervisorId } = supervisorId