{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}

module Handler.ExamOffice.Exams
  ( getEOExamsR
  ) where

import Import

import Handler.Utils
import qualified Handler.Utils.ExamOffice.Exam as Exam
import qualified Handler.Utils.ExamOffice.ExternalExam as ExternalExam

import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E

import qualified Colonnade


type ExamsTableExpr = (               E.SqlExpr (Maybe (Entity Exam))
                        `E.InnerJoin` E.SqlExpr (Maybe (Entity Course))
                      )
    `E.FullOuterJoin` E.SqlExpr (Maybe (Entity ExternalExam))

type ExamsTableData = DBRow ( Either (Entity ExternalExam) (Entity Exam, Entity Course)
                            , Natural, Natural
                            )

queryExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity Exam)))
queryExam = to $ $(E.sqlIJproj 2 1) . $(E.sqlFOJproj 2 1)

queryCourse :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity Course)))
queryCourse = to $ $(E.sqlIJproj 2 2) . $(E.sqlFOJproj 2 1)

queryExternalExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExternalExam)))
queryExternalExam = to $(E.sqlFOJproj 2 2)

querySynchronised :: E.SqlExpr (E.Value UserId) -> Getter ExamsTableExpr (E.SqlExpr (E.Value Natural))
querySynchronised office = to . runReader $ do
  exam' <- view queryExam
  externalExam' <- view queryExternalExam
  let
    examSynchronised examId = E.subSelectCount . E.from $ \examResult -> do
      E.where_ $ examResult E.^. ExamResultExam E.==. examId
      E.where_ $ Exam.examOfficeExamResultAuth office examResult
      E.where_ $ Exam.resultIsSynced office examResult
    externalExamSynchronised externalExamId = E.subSelectCount . E.from $ \externalExamResult -> do
      E.where_ $ externalExamResult E.^. ExternalExamResultExam E.==. externalExamId
      E.where_ $ ExternalExam.examOfficeExternalExamResultAuth office externalExamResult
      E.where_ $ ExternalExam.resultIsSynced office externalExamResult
  return $ E.maybe (E.val 0) examSynchronised (exam' E.?. ExamId) E.+. E.maybe (E.val 0) externalExamSynchronised (externalExam' E.?. ExternalExamId)

queryResults :: E.SqlExpr (E.Value UserId) -> Getter ExamsTableExpr (E.SqlExpr (E.Value Natural))
queryResults office = to . runReader $ do
  exam' <- view queryExam
  externalExam' <- view queryExternalExam
  let
    results examId = E.subSelectCount . E.from $ \examResult -> do
      E.where_ $ examResult E.^. ExamResultExam E.==. examId
      E.where_ $ Exam.examOfficeExamResultAuth office examResult
    externalResults externalExamId = E.subSelectCount . E.from $ \externalExamResult -> do
      E.where_ $ externalExamResult E.^. ExternalExamResultExam E.==. externalExamId
      E.where_ $ ExternalExam.examOfficeExternalExamResultAuth office externalExamResult
  return $ E.maybe (E.val 0) results (exam' E.?. ExamId) E.+. E.maybe (E.val 0) externalResults (externalExam' E.?. ExternalExamId)

queryIsSynced :: UTCTime -> E.SqlExpr (E.Value UserId) -> Getter ExamsTableExpr (E.SqlExpr (E.Value Bool))
queryIsSynced now office = to . runReader $ do
  exam' <- view queryExam
  externalExam' <- view queryExternalExam
  let
    examSynchronised examId = E.not_ . E.exists . E.from $ \examResult -> do
      E.where_ $ examResult E.^. ExamResultExam E.==. examId
      E.where_ $ Exam.examOfficeExamResultAuth office examResult
      E.where_ . E.not_ $ Exam.resultIsSynced office examResult
    externalExamSynchronised externalExamId = E.not_ . E.exists . E.from $ \externalExamResult -> do
      E.where_ $ externalExamResult E.^. ExternalExamResultExam E.==. externalExamId
      E.where_ $ ExternalExam.examOfficeExternalExamResultAuth office externalExamResult
      E.where_ . E.not_ $ ExternalExam.resultIsSynced office externalExamResult
    open examClosed' = E.maybe E.true (E.>. E.val now) examClosed'
  return $ E.maybe E.false examSynchronised (exam' E.?. ExamId) E.||. E.maybe E.false open (exam' E.?. ExamClosed) E.||. E.maybe E.false externalExamSynchronised (externalExam' E.?. ExternalExamId)


resultExam :: Traversal' ExamsTableData (Entity Exam)
resultExam = _dbrOutput . _1 . _Right . _1

resultCourse :: Traversal' ExamsTableData (Entity Course)
resultCourse = _dbrOutput . _1 . _Right . _2

resultExternalExam :: Traversal' ExamsTableData (Entity ExternalExam)
resultExternalExam = _dbrOutput . _1 . _Left

resultSynchronised, resultResults :: Lens' ExamsTableData Natural
resultSynchronised = _dbrOutput . _2
resultResults = _dbrOutput . _3

resultIsSynced :: Getter ExamsTableData Bool
resultIsSynced = to $ (>=) <$> view resultSynchronised <*> view resultResults


-- | List of all exams where the current user may (in her function as
--   exam-office) access users grades
getEOExamsR :: Handler Html
getEOExamsR = do
  uid <- requireAuthId
  now <- liftIO getCurrentTime

  examsTable <- runDB $ do
    let
      examLink :: Course -> Exam -> SomeRoute UniWorX
      examLink Course{..} Exam{..}
        = SomeRoute $ CExamR courseTerm courseSchool courseShorthand examName EGradesR

      courseLink :: Course -> SomeRoute UniWorX
      courseLink Course{..}
        = SomeRoute $ CourseR courseTerm courseSchool courseShorthand CShowR

      externalExamLink :: ExternalExam -> SomeRoute UniWorX
      externalExamLink ExternalExam{..}
        = SomeRoute $ EExamR externalExamTerm externalExamSchool externalExamCourseName externalExamExamName EEGradesR

      querySynchronised' = querySynchronised $ E.val uid
      queryResults' = queryResults $ E.val uid
      queryIsSynced' = queryIsSynced now $ E.val uid

      examsDBTable = DBTable{..}
        where
          dbtSQLQuery = runReaderT $ do
            exam <- view queryExam
            course <- view queryCourse
            externalExam <- view queryExternalExam

            synchronised <- view querySynchronised'
            results <- view queryResults'

            lift $ do
              E.on   E.false
              E.on $ exam E.?. ExamCourse E.==. course E.?. CourseId

              E.where_ $ results E.>. E.val 0
              E.where_ $ (E.not_ (E.isNothing $ exam E.?. ExamId) E.&&. E.not_ (E.isNothing $ course E.?. CourseId) E.&&.         E.isNothing  (externalExam E.?. ExternalExamId))
                   E.||. (        E.isNothing  (exam E.?. ExamId) E.&&.         E.isNothing  (course E.?. CourseId) E.&&. E.not_ (E.isNothing $ externalExam E.?. ExternalExamId))

            return (exam, course, externalExam, synchronised, results)
          dbtRowKey = views ($(multifocusG 2) queryExam queryExternalExam) (bimap (E.?. ExamId) (E.?. ExternalExamId))

          dbtProj :: DBRow _ -> DB ExamsTableData
          dbtProj = runReaderT $ (asks . set _dbrOutput) <=< magnify _dbrOutput $ do
            exam <- view _1
            course <- view _2
            externalExam <- view _3

            case (exam, course, externalExam) of
              (Just exam', Just course', Nothing) ->
                (Right (exam', course'),,) <$> view (_4 . _Value) <*> view (_5 . _Value)
              (Nothing, Nothing, Just externalExam') ->
                (Left externalExam',,) <$> view (_4 . _Value) <*> view (_5 . _Value)
              _other -> return $ error "Got exam & externalExam in same result"


          colSynced = Colonnade.singleton (fromSortable . Sortable (Just "synced") $ i18nCell MsgExamSynchronised) $ \x -> flip runReader x $ do
            mExam <- preview resultExam

            if
              | Just (Entity _ Exam{examClosed}) <- mExam
              , NTop examClosed > NTop (Just now)
                -> return . cell $ toWidget iconNew
              | otherwise
                -> do
                  synced <- view resultSynchronised
                  results <- view resultResults
                  isSynced <- view resultIsSynced

                  return $ cell
                     [whamlet|
                       $newline never
                       $if isSynced
                         #{iconOK}
                       $else
                         #{synced}/#{results}
                     |]
                     & cellAttrs <>~ [ ("class", "heated")
                                     , ("style", [st|--hotness: #{tshow (heat results synced)}|])
                                     ]


          dbtColonnade :: Colonnade Sortable _ _
          dbtColonnade = mconcat
            [ colSynced
            , maybeAnchorColonnade ( runMaybeT $     mpreview ($(multifocusG 2) (pre $ resultCourse . _entityVal) (pre $ resultExam . _entityVal) . to (uncurry $ liftA2 examLink) . _Just)
                                                 <|> mpreviews (resultExternalExam . _entityVal) externalExamLink
                                   )
                $ emptyOpticColonnade (resultExam . _entityVal . _examName <> resultExternalExam . _entityVal . _externalExamExamName) colExamName
            , emptyOpticColonnade (resultExam . _entityVal . $(multifocusG 2) _examStart _examEnd) colExamTime
            , emptyOpticColonnade (resultExam . _entityVal . _examFinished) colExamFinishedOffice
            , emptyOpticColonnade (resultExam . _entityVal . _examClosed) colExamClosed
            , maybeAnchorColonnade (previews (resultCourse . _entityVal) courseLink)
                $ emptyOpticColonnade (resultCourse . _entityVal . _courseName <> resultExternalExam . _entityVal . _externalExamCourseName) colCourseName
            , emptyOpticColonnade (resultCourse . _entityVal . _courseSchool <> resultExternalExam . _entityVal . _externalExamSchool) colSchool
            , emptyOpticColonnade (resultCourse . _entityVal . _courseTerm <> resultExternalExam . _entityVal . _externalExamTerm) colTermShort
            ]
          dbtSorting = mconcat
            [ singletonMap "synced" . SortColumn $ (E./.) <$> view querySynchronised' <*> view queryResults'
            , singletonMap "is-synced" . SortColumn $ view queryIsSynced'
            , sortExamName (to $ E.unsafeCoalesce . sequence [views queryExam (E.?. ExamName), views queryExternalExam (E.?. ExternalExamExamName)])
            , sortExamTime (queryExam . $(multifocusG 2) (to $ E.joinV . (E.?. ExamStart)) (to $ E.joinV . (E.?. ExamEnd)))
            , sortExamFinished (queryExam . to (E.joinV . (E.?. ExamFinished)))
            , sortExamClosed (queryExam . to (E.joinV . (E.?. ExamClosed)))
            , sortCourseName (to $ E.unsafeCoalesce . sequence [views queryCourse (E.?. CourseName), views queryExternalExam (E.?. ExternalExamCourseName)])
            , sortSchool (to $ E.unsafeCoalesce . sequence [views queryCourse (E.?. CourseSchool), views queryExternalExam (E.?. ExternalExamSchool)])
            , sortTerm (to $ E.unsafeCoalesce . sequence [views queryCourse (E.?. CourseTerm), views queryExternalExam (E.?. ExternalExamTerm)])
            ]

          dbtFilter = mconcat
            [ singletonMap "may-access" . FilterProjected $ \(Any b) r -> (== b) <$> if
                | Just exam <- r ^? resultExam . _entityVal
                , Just course <- r ^? resultCourse . _entityVal
                -> hasReadAccessTo . urlRoute $ examLink course exam
                | Just eexam <- r ^? resultExternalExam . _entityVal
                -> hasReadAccessTo . urlRoute $ externalExamLink eexam :: DB Bool
                | otherwise
                -> return $ error "Got neither exam nor externalExam in result"
            ]
          dbtFilterUI = mconcat
            [
            ]

          dbtStyle = def -- { dbsFilterLayout = defaultDBSFilterLayout }
          dbtParams = def

          dbtIdent :: Text
          dbtIdent = "exams"

          dbtCsvEncode = noCsvEncode
          dbtCsvDecode = Nothing

      examsDBTableValidator = def
        & defaultSorting [SortAscBy "is-synced", SortAscBy "exam-time"]
        & forceFilter "may-access" (Any True)

    dbTableWidget' examsDBTableValidator examsDBTable

  siteLayoutMsg MsgMenuExamList $ do
    setTitleI MsgMenuExamList
    examsTable
