module Jobs.Handler.QueueNotification
  ( dispatchJobQueueNotification
  ) where

import Import

import Jobs.Types

import qualified Database.Esqueleto.Legacy as E
import qualified Database.Esqueleto.Utils as E
import Jobs.Queue

import qualified Data.Set as Set

import Handler.Utils.ExamOffice.Exam
import Handler.Utils.ExamOffice.ExternalExam
import Handler.Utils.Allocation (allocationNotifyNewCourses)

import qualified Data.Conduit.Combinators as C


dispatchJobQueueNotification :: Notification -> JobHandler UniWorX
dispatchJobQueueNotification jNotification = JobHandlerAtomic $
  runConduit $ yield jNotification
            .| transPipe (hoist lift) determineNotificationCandidates
            .| C.filterM (\(notification', override, Entity _ User{userNotificationSettings}) -> or2M (return override) $ notificationAllowed userNotificationSettings <$> hoist lift (classifyNotification notification'))
            .| C.map (\(notification', _, Entity uid _) -> JobSendNotification uid notification')
            .| sinkDBJobs


determineNotificationCandidates :: ConduitT Notification (Notification, Bool, Entity User) DB ()
determineNotificationCandidates = awaitForever $ \notif -> do
  let withNotif :: ConduitT () (Entity User) DB () -> ConduitT Notification (Notification, Bool, Entity User) DB ()
      withNotif c = toProducer c .| C.map (notif, False, )

      withNotifOverride :: ConduitT () (E.Value Bool, Entity User) DB () -> ConduitT Notification (Notification, Bool, Entity User) DB ()
      withNotifOverride c = toProducer c .| C.map (\(E.Value override, user) -> (notif, override, user))

      -- | Assumes that conduit produces output sorted by `UserId`
      separateTargets :: Ord target
                      => (Set target -> Notification)
                      -> ConduitT () (Entity User, E.Value target) DB ()
                      -> ConduitT Notification (Notification, Bool, Entity User) DB ()
      separateTargets mkNotif' c = toProducer c .| go Nothing Set.empty
        where go Nothing _ = do
                next <- await
                case next of
                  Nothing -> return ()
                  Just (uent, E.Value t) -> go (Just uent) $ Set.singleton t
              go (Just uent) ts = do
                next <- await
                case next of
                  Nothing -> yield (mkNotif' ts, False, uent)
                  Just next'@(uent', E.Value t)
                    | ((==) `on` entityKey) uent uent' -> go (Just uent) $ Set.insert t ts
                    | otherwise -> yield (mkNotif' ts, False, uent) >> leftover next' >> go Nothing Set.empty
                
  case notif of
    NotificationSubmissionRated{..}
      -> withNotif . E.selectSource . E.from $ \(user `E.InnerJoin` submissionUser) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId] $ do
           E.on $ user E.^. UserId E.==. submissionUser E.^. SubmissionUserUser
           E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val nSubmission
           return user
    NotificationSheetActive{..}
      -> withNotif . E.selectSource . E.from $ \(user `E.InnerJoin` courseParticipant `E.InnerJoin` sheet) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId] $ do
           E.on $ sheet E.^. SheetCourse E.==. courseParticipant E.^. CourseParticipantCourse
           E.on $ user E.^. UserId E.==. courseParticipant E.^. CourseParticipantUser
           E.where_ $ courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
           E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
           return user
    NotificationSheetHint{..}
      -> withNotif . E.selectSource . E.from $ \(user `E.InnerJoin` courseParticipant `E.InnerJoin` sheet) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId] $ do
           E.on $ sheet E.^. SheetCourse E.==. courseParticipant E.^. CourseParticipantCourse
           E.on $ user E.^. UserId E.==. courseParticipant E.^. CourseParticipantUser
           E.where_ $ courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
           E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
           return user
    NotificationSheetSolution{..}
      -> withNotif . E.selectSource . E.from $ \(user `E.InnerJoin` courseParticipant `E.InnerJoin` sheet) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId] $ do
           E.on $ sheet E.^. SheetCourse E.==. courseParticipant E.^. CourseParticipantCourse
           E.on $ user E.^. UserId E.==. courseParticipant E.^. CourseParticipantUser
           E.where_ $ courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
           E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
           return user
    NotificationSheetSoonInactive{..}
      -> withNotif . E.selectSource . E.from $ \(user `E.InnerJoin` courseParticipant `E.InnerJoin` sheet) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId] $ do
           E.on $ sheet E.^. SheetCourse E.==. courseParticipant E.^. CourseParticipantCourse
           E.on $ user E.^. UserId E.==. courseParticipant E.^. CourseParticipantUser
           E.where_ $ courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
           E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
           return user
    NotificationSheetInactive{..}
      -> withNotif . E.selectSource . E.from $ \(user `E.InnerJoin` lecturer `E.InnerJoin` sheet) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId] $ do
           E.on $ lecturer E.^. LecturerCourse E.==. sheet E.^. SheetCourse
           E.on $ lecturer E.^. LecturerUser   E.==. user E.^. UserId
           E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
           return user
    NotificationCorrectionsAssigned{..}
      -> withNotif $ selectSource [UserId ==. nUser] []
    NotificationCorrectionsNotDistributed{nSheet}
      -> withNotif . E.selectSource . E.from $ \(user `E.InnerJoin` lecturer `E.InnerJoin` sheet) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId] $ do
           E.on $ lecturer E.^. LecturerCourse E.==. sheet E.^. SheetCourse
           E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId
           E.where_ $ sheet E.^. SheetId E.==. E.val nSheet
           return user
    NotificationUserRightsUpdate{..}
      -> do
           -- always send to affected user
           affectedUser <- lift $ selectList [UserId ==. nUser] []
           -- send to same-school admins only if there was an update
           currentAdminSchools <- lift $ setOf (folded . _entityVal . _userFunctionSchool) <$> selectList [UserFunctionUser ==. nUser, UserFunctionFunction ==. SchoolAdmin] []
           let oldAdminSchools = setOf (folded . filtered ((== SchoolAdmin) . view _1) . _2 . from _SchoolId) nOriginalRights
               newAdminSchools = currentAdminSchools `Set.difference` oldAdminSchools
           affectedAdmins <- lift . E.select . E.from $ \(user `E.InnerJoin` admin) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId] $ do
             E.on $ admin E.^. UserFunctionUser E.==. user E.^. UserId
             E.where_ $ admin E.^. UserFunctionSchool `E.in_` E.valList (Set.toList newAdminSchools)
                  E.&&. admin E.^. UserFunctionFunction E.==. E.val SchoolAdmin
             return user
           withNotif . yieldMany . nubOrd $ affectedUser <> affectedAdmins
    NotificationUserSystemFunctionsUpdate{..}
      -> withNotif $ selectSource [UserId ==. nUser] []
    NotificationUserAuthModeUpdate{..}
      -> withNotif $ selectSource [UserId ==. nUser] []
    NotificationExamRegistrationActive{..}
      -> withNotif . E.selectSource . E.from $ \(exam `E.InnerJoin` courseParticipant `E.InnerJoin` user) -> do
           E.on $ courseParticipant E.^. CourseParticipantUser E.==. user E.^. UserId
           E.on $ courseParticipant E.^. CourseParticipantCourse E.==. exam E.^. ExamCourse
           E.where_ $ exam E.^. ExamId E.==. E.val nExam
           E.where_ . E.not_ . E.exists . E.from $ \examRegistration ->
             E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. user E.^. UserId
                  E.&&. examRegistration E.^. ExamRegistrationExam E.==. E.val nExam
           E.where_ $ courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
           return user
    NotificationExamRegistrationSoonInactive{..}
      -> withNotif . E.selectSource . E.from $ \(exam `E.InnerJoin` courseParticipant `E.InnerJoin` user) -> do
           E.on $ courseParticipant E.^. CourseParticipantUser E.==. user E.^. UserId
           E.on $ courseParticipant E.^. CourseParticipantCourse E.==. exam E.^. ExamCourse
           E.where_ $ exam E.^. ExamId E.==. E.val nExam
           E.where_ . E.not_ . E.exists . E.from $ \examRegistration ->
             E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. user E.^. UserId
                  E.&&. examRegistration E.^. ExamRegistrationExam E.==. E.val nExam
           E.where_ $ courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
           return user
    NotificationExamDeregistrationSoonInactive{..}
      -> withNotif . E.selectSource . E.from $ \(examRegistration `E.InnerJoin` user) -> do
           E.on $ examRegistration E.^. ExamRegistrationUser E.==. user E.^. UserId
           E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val nExam
           return user
    NotificationExamResult{..}
      -> do
           lastExec <- lift . fmap (fmap $ cronLastExecTime . entityVal) . getBy . UniqueCronLastExec . toJSON $ JobQueueNotification notif
           withNotif . E.selectSource . E.from $ \(examResult `E.InnerJoin` user) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId] $ do
             E.on $ examResult E.^. ExamResultUser E.==. user E.^. UserId
             E.where_ $ examResult E.^. ExamResultExam E.==. E.val nExam
             whenIsJust lastExec $ \lastExec' ->
               E.where_ $ examResult E.^. ExamResultLastChanged E.>. E.val lastExec'
             return user
    NotificationAllocationStaffRegister{..}
      -> separateTargets NotificationAllocationStaffRegister . E.selectSource . E.from $ \(user `E.InnerJoin` userFunction `E.InnerJoin` allocation) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId, E.asc $ allocation E.^. AllocationId] $ do
           E.on $ userFunction E.^. UserFunctionSchool E.==. allocation E.^. AllocationSchool
           E.on $ user E.^. UserId E.==. userFunction E.^. UserFunctionUser

           E.where_ $ userFunction E.^. UserFunctionFunction E.==. E.val SchoolLecturer
                E.&&. allocation E.^. AllocationId `E.in_` E.valList (Set.toList nAllocations)

           E.where_ . E.exists . E.from $ \userSchool ->
             E.where_ $ userSchool E.^. UserSchoolUser E.==. user E.^. UserId
                  E.&&. userSchool E.^. UserSchoolSchool E.==. allocation E.^. AllocationSchool
                  E.&&. E.not_ (userSchool E.^. UserSchoolIsOptOut)

           E.where_ . E.not_ . E.exists . E.from $ \(course `E.InnerJoin` lecturer) -> do
             E.on $ lecturer E.^. LecturerCourse E.==. course E.^. CourseId
              E.&&. lecturer E.^. LecturerUser E.==. user E.^. UserId
             E.where_ . E.exists . E.from $ \allocationCourse ->
               E.where_ $ allocationCourse E.^. AllocationCourseCourse E.==. course E.^. CourseId
                    E.&&. allocationCourse E.^. AllocationCourseAllocation E.==. allocation E.^. AllocationId

           return (user, allocation E.^. AllocationId)
    NotificationAllocationRegister{..}
      -> separateTargets NotificationAllocationRegister . E.selectSource . E.from $ \(user `E.InnerJoin` allocation) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId, E.asc $ allocation E.^. AllocationId] $ do
           E.on   E.true

           E.where_ $ allocation E.^. AllocationId `E.in_` E.valList (Set.toList nAllocations)

           E.where_ . E.exists . E.from $ \userSchool ->
             E.where_ $ userSchool E.^. UserSchoolUser E.==. user E.^. UserId
                  E.&&. userSchool E.^. UserSchoolSchool E.==. allocation E.^. AllocationSchool
                  E.&&. E.not_ (userSchool E.^. UserSchoolIsOptOut)

           E.where_ . E.not_ . E.exists . E.from $ \application ->
             E.where_ $ application E.^. CourseApplicationAllocation E.==. E.just (allocation E.^. AllocationId)
                  E.&&. application E.^. CourseApplicationUser E.==. user E.^. UserId

           return (user, allocation E.^. AllocationId)
    NotificationAllocationAllocation{..}
      -> separateTargets NotificationAllocationAllocation . E.selectSource . E.from $ \(user `E.InnerJoin` lecturer `E.InnerJoin` course `E.InnerJoin` allocationCourse) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId, E.asc $ allocationCourse E.^. AllocationCourseAllocation] $ do
           E.on $ allocationCourse E.^. AllocationCourseCourse E.==. course E.^. CourseId
           E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
           E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId

           E.where_ $ allocationCourse E.^. AllocationCourseAllocation `E.in_` E.valList (Set.toList nAllocations)

           E.where_ . E.not_ . E.exists . E.from $ \application ->
             E.where_ $ application E.^. CourseApplicationAllocation E.==. E.just (allocationCourse E.^. AllocationCourseAllocation)
                  E.&&. application E.^. CourseApplicationCourse     E.==. course E.^. CourseId
                  E.&&. application E.^. CourseApplicationUser       E.!=. user E.^. UserId
                  E.&&. E.not_ (E.isNothing $ application E.^. CourseApplicationRatingTime)

           E.where_ . E.exists . E.from $ \application ->
             E.where_ $ application E.^. CourseApplicationAllocation E.==. E.just (allocationCourse E.^. AllocationCourseAllocation)
                  E.&&. application E.^. CourseApplicationCourse     E.==. course E.^. CourseId
                  E.&&. application E.^. CourseApplicationUser       E.!=. user E.^. UserId

           return (user, allocationCourse E.^. AllocationCourseAllocation)
    NotificationAllocationUnratedApplications{..}
      -> separateTargets NotificationAllocationUnratedApplications . E.selectSource . E.from $ \(user `E.InnerJoin` lecturer `E.InnerJoin` course `E.InnerJoin` allocationCourse) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId, E.asc $ allocationCourse E.^. AllocationCourseAllocation] $ do
           E.on $ allocationCourse E.^. AllocationCourseCourse E.==. course E.^. CourseId
           E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
           E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId

           E.where_ $ allocationCourse E.^. AllocationCourseAllocation `E.in_` E.valList (Set.toList nAllocations)

           E.where_ . E.exists . E.from $ \application ->
             E.where_ $ application E.^. CourseApplicationAllocation E.==. E.just (allocationCourse E.^. AllocationCourseAllocation)
                  E.&&. application E.^. CourseApplicationCourse     E.==. course E.^. CourseId
                  E.&&. application E.^. CourseApplicationUser       E.!=. user E.^. UserId
                  E.&&. E.isNothing (application E.^. CourseApplicationRatingTime)

           return (user, allocationCourse E.^. AllocationCourseAllocation)
    NotificationExamOfficeExamResults{..}
      -> withNotif . E.selectSource . E.from $ \user -> do
           E.where_ . E.exists . E.from $ \examResult -> do
             E.where_ $ examResult E.^. ExamResultExam E.==. E.val nExam
             E.where_ $ examOfficeExamResultAuth (user E.^. UserId) examResult
           return user
    NotificationExamOfficeExamResultsChanged{..}
      -> withNotif . E.selectSource . E.from $ \user -> do
           E.where_ . E.exists . E.from $ \examResult -> do
             E.where_ $ examResult E.^. ExamResultId `E.in_` E.valList (Set.toList nExamResults)
             E.where_ $ examOfficeExamResultAuth (user E.^. UserId) examResult
           return user
    NotificationExamOfficeExternalExamResults{..}
      -> withNotif . E.selectSource . E.from $ \user -> do
           E.where_ . E.exists . E.from $ \externalExamResult -> do
             E.where_ $ externalExamResult E.^. ExternalExamResultExam E.==. E.val nExternalExam
             E.where_ $ examOfficeExternalExamResultAuth (user E.^. UserId) externalExamResult
           return user
    NotificationAllocationResults{..}
      -> do
           lastExec <- lift . fmap (fmap $ cronLastExecTime . entityVal) . getBy . UniqueCronLastExec . toJSON $ JobQueueNotification notif
           withNotif . E.selectSource . E.from $ \user -> do
             let isStudent = E.exists . E.from $ \application ->
                   E.where_ $ application E.^. CourseApplicationAllocation E.==. E.just (E.val nAllocation)
                        E.&&. application E.^. CourseApplicationUser E.==. user E.^. UserId
                 isLecturer = E.exists . E.from $ \(lecturer `E.InnerJoin` allocationCourse) ->
                   E.on $ lecturer E.^. LecturerCourse E.==. allocationCourse E.^. AllocationCourseCourse
                    E.&&. allocationCourse E.^. AllocationCourseAllocation E.==. E.val nAllocation
                    E.&&. lecturer E.^. LecturerUser E.==. user E.^. UserId

                 wasAllocated t = E.exists . E.from $ \participant ->
                   E.where_ $ participant E.^. CourseParticipantUser E.==. user E.^. UserId
                        E.&&. participant E.^. CourseParticipantAllocated E.==. E.just (E.val nAllocation)
                        E.&&. participant E.^. CourseParticipantRegistration E.>. E.val t
                        E.&&. participant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
                 hasAllocations t = E.exists . E.from $ \(lecturer `E.InnerJoin` participant) -> do
                   E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId
                    E.&&. lecturer E.^. LecturerCourse E.==. participant E.^. CourseParticipantCourse
                   E.where_ $ participant E.^. CourseParticipantAllocated E.==. E.just (E.val nAllocation)
                        E.&&. participant E.^. CourseParticipantRegistration E.>. E.val t
                        E.&&. participant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
             case lastExec of
               Nothing -> E.where_ $ isStudent E.||. isLecturer
               Just t  -> E.where_ $ wasAllocated t E.||. hasAllocations t

             return user
    NotificationCourseRegistered{..}
     -> withNotif . yieldMMany $ getEntity nUser
    NotificationSubmissionEdited{..}
     -> withNotif . E.selectSource . E.from $ \(user `E.InnerJoin` submissionUser) -> do
          E.on $ user E.^. UserId E.==. submissionUser E.^. SubmissionUserUser
          E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val nSubmission
               E.&&. user E.^. UserId E.!=. E.val nInitiator
          return user
    NotificationSubmissionUserCreated{..}
      -> withNotif . yieldMMany $ getEntity nUser
    NotificationSubmissionUserDeleted{..}
      -> withNotif . yieldMMany $ getEntity nUser
    NotificationAllocationNewCourse{..}
      -> withNotifOverride . E.selectSource . E.from $ \user -> do
           let hasOverride = E.exists . E.from $ \allocationNotificationSetting ->
                 E.where_ $ allocationNotificationSetting E.^. AllocationNotificationSettingUser E.==. user E.^. UserId
                      E.&&. allocationNotificationSetting E.^. AllocationNotificationSettingAllocation E.==. E.val nAllocation
                      E.&&. E.not_ (allocationNotificationSetting E.^. AllocationNotificationSettingIsOptOut)

           E.where_ . allocationNotifyNewCourses (E.val nAllocation) $ user E.^. UserId

           E.where_ . E.not_ . E.exists . E.from $ \application ->
             E.where_ $ application E.^. CourseApplicationAllocation E.==. E.justVal nAllocation
                  E.&&. application E.^. CourseApplicationUser E.==. user E.^. UserId
                  E.&&. application E.^. CourseApplicationCourse E.==. E.val nCourse
           
           return (hasOverride, user)


classifyNotification :: Notification -> DB NotificationTrigger
classifyNotification NotificationSubmissionRated{..} = do
  Sheet{sheetType} <- belongsToJust submissionSheet =<< getJust nSubmission
  return $ case sheetType of
    NotGraded -> NTSubmissionRated
    _other -> NTSubmissionRatedGraded
classifyNotification NotificationSheetActive{}                    = return NTSheetActive
classifyNotification NotificationSheetHint{}                      = return NTSheetHint
classifyNotification NotificationSheetSolution{}                  = return NTSheetSolution
classifyNotification NotificationSheetSoonInactive{}              = return NTSheetSoonInactive
classifyNotification NotificationSheetInactive{}                  = return NTSheetInactive
classifyNotification NotificationCorrectionsAssigned{}            = return NTCorrectionsAssigned
classifyNotification NotificationCorrectionsNotDistributed{}      = return NTCorrectionsNotDistributed
classifyNotification NotificationUserRightsUpdate{}               = return NTUserRightsUpdate
classifyNotification NotificationUserSystemFunctionsUpdate{}      = return NTUserRightsUpdate
classifyNotification NotificationUserAuthModeUpdate{}             = return NTUserAuthModeUpdate
classifyNotification NotificationExamRegistrationActive{}         = return NTExamRegistrationActive
classifyNotification NotificationExamRegistrationSoonInactive{}   = return NTExamRegistrationSoonInactive
classifyNotification NotificationExamDeregistrationSoonInactive{} = return NTExamDeregistrationSoonInactive
classifyNotification NotificationExamResult{}                     = return NTExamResult
classifyNotification NotificationAllocationStaffRegister{}        = return NTAllocationStaffRegister
classifyNotification NotificationAllocationAllocation{}           = return NTAllocationAllocation
classifyNotification NotificationAllocationRegister{}             = return NTAllocationRegister
classifyNotification NotificationAllocationUnratedApplications{}  = return NTAllocationUnratedApplications
classifyNotification NotificationExamOfficeExamResults{}          = return NTExamOfficeExamResults
classifyNotification NotificationExamOfficeExamResultsChanged{}   = return NTExamOfficeExamResultsChanged
classifyNotification NotificationExamOfficeExternalExamResults{}  = return NTExamOfficeExamResults
classifyNotification NotificationAllocationResults{}              = return NTAllocationResults
classifyNotification NotificationCourseRegistered{}               = return NTCourseRegistered
classifyNotification NotificationSubmissionEdited{}               = return NTSubmissionEdited
classifyNotification NotificationSubmissionUserCreated{}          = return NTSubmissionUserCreated
classifyNotification NotificationSubmissionUserDeleted{}          = return NTSubmissionUserDeleted
classifyNotification NotificationAllocationNewCourse{}            = return NTAllocationNewCourse
