module Handler.Utils.Workflow.Workflow
  ( ensureScope
  , followEdge
  , followAutomaticEdges, WorkflowAutomaticEdgeException(..)
  , sourceWorkflowActionInfos
  , module Handler.Utils.Workflow.Restriction
  ) where

import Import

import Utils.Workflow
import Handler.Utils.Workflow.EdgeForm
import Handler.Utils.Workflow.Restriction

import qualified Data.Set as Set
import qualified Data.Map as Map

import qualified Data.Conduit.Combinators as C


ensureScope :: IdWorkflowScope -> CryptoFileNameWorkflowWorkflow -> MaybeT DB WorkflowWorkflowId
ensureScope wiScope cID = do
  wId <- catchMaybeT (Proxy @CryptoIDError) $ decrypt cID
  WorkflowWorkflow{..} <- MaybeT $ get wId
  let wiScope' = wiScope
        & _wisTerm   %~ unTermKey
        & _wisSchool %~ unSchoolKey
        & _wisCourse %~ view _SqlKey
  guard $ workflowWorkflowScope == wiScope'
  return wId

followEdge :: ( MonadHandler m
              , HandlerSite m ~ UniWorX
              , MonadThrow m
              )
           => IdWorkflowGraph -> WorkflowEdgeForm -> Maybe IdWorkflowState -> m IdWorkflowState
-- | Remember to invalidate auth cache
followEdge graph edgeRes cState = do
  act <- workflowEdgeFormToAction edgeRes
  followAutomaticEdges graph $ maybe id (<>) cState (act `ncons` mempty)

data WorkflowAutomaticEdgeException
  = WorkflowAutomaticEdgeCycle [(WorkflowGraphEdgeLabel, WorkflowGraphNodeLabel)]
  | WorkflowAutomaticEdgeAmbiguity (Set (WorkflowGraphEdgeLabel, WorkflowGraphNodeLabel))
  deriving (Eq, Ord, Read, Show, Generic, Typeable)
  deriving anyclass (Exception)

followAutomaticEdges :: forall m.
                        ( MonadIO m
                        , MonadThrow m
                        )
                     => IdWorkflowGraph -> IdWorkflowState -> m IdWorkflowState
followAutomaticEdges WorkflowGraph{..} = go []
  where
    go :: [((WorkflowGraphNodeLabel, Set WorkflowPayloadLabel), (WorkflowGraphEdgeLabel, WorkflowGraphNodeLabel))] -- ^ Should encode all state from which automatic edges decide whether they can be followed
       -> IdWorkflowState
       -> m IdWorkflowState
    go automaticEdgesTaken history
      | null automaticEdgeOptions = return history
      | [(edgeLbl, nodeLbl)] <- automaticEdgeOptions = if
          | (edgeDecisionInput, (edgeLbl, nodeLbl)) `elem` automaticEdgesTaken
            -> throwM . WorkflowAutomaticEdgeCycle . reverse $ map (view _2) automaticEdgesTaken
          | otherwise -> do
              wpTime <- liftIO getCurrentTime
              let wpUser = Nothing
                  wpPayload = mempty
                  wpTo = nodeLbl
                  wpVia = edgeLbl
              go ((edgeDecisionInput, (edgeLbl, nodeLbl)) : automaticEdgesTaken) $ history <> (WorkflowAction{..} `ncons` mempty)
      | otherwise = throwM . WorkflowAutomaticEdgeAmbiguity $ Set.fromList automaticEdgeOptions
      where
        cState = wpTo $ last history
        automaticEdgeOptions = nubOrd $ do
          (nodeLbl, WGN{..}) <- Map.toList wgNodes
          (edgeLbl, WorkflowGraphEdgeAutomatic{..}) <- Map.toList wgnEdges
          guard $ wgeSource == cState
          whenIsJust wgeRestriction $ guard . checkWorkflowRestriction (Just history)
          return (edgeLbl, nodeLbl)
        filledPayloads = Map.keysSet . Map.filter (not . Set.null) $ workflowStateCurrentPayloads history
        edgeDecisionInput = (cState, filledPayloads)


sourceWorkflowActionInfos
  :: forall backend m.
     ( MonadHandler m, HandlerSite m ~ UniWorX
     , BackendCompatible SqlReadBackend backend
     , MonadCrypto m, MonadCryptoKey m ~ CryptoIDKey
     , MonadCatch m, MonadUnliftIO m
     , MonadAP (ReaderT backend m)
     )
  => WorkflowWorkflowId
  -> WorkflowState FileReference UserId
  -> ConduitT () (WorkflowActionInfo FileReference UserId) (ReaderT backend m) ()
-- ^ Does `mayViewWorkflowAction`
sourceWorkflowActionInfos wwId wState = do
  mAuthId <- maybeAuthId
  let authCheck WorkflowActionInfo{..}
        = mayViewWorkflowAction mAuthId wwId waiAction
  yieldMany (workflowActionInfos wState) .| C.filterM authCheck
