-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later

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

module Handler.ExamOffice.Exams
  ( getEOExamsR, postEOExamsR
  ) 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.Legacy as E
import qualified Database.Esqueleto.Utils as E

import qualified Colonnade

import qualified Data.Conduit.Combinators as C
import qualified Data.Map as Map
import qualified Data.Set as Set


data ExamAction = ExamSetLabel | ExamRemoveLabel
  deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
  deriving anyclass (Universe, Finite)

nullaryPathPiece ''ExamAction $ camelToPathPiece' 1
embedRenderMessage ''UniWorX ''ExamAction id

data ExamActionData = ExamSetLabelData
                        { easlNewLabel :: ExamOfficeLabelId
                        }
                    | ExamRemoveLabelData
  deriving (Eq, Ord, Read, Show, Generic)


data ExamsTableFilterProj = ExamsTableFilterProj
  { etProjFilterMayAccess  :: Maybe Bool
  , etProjFilterHasResults :: Maybe Bool
  , etProjFilterLabel      :: Maybe (Either ExamOfficeExternalExamLabelId ExamOfficeExamLabelId)
  , etProjFilterIsSynced   :: Maybe Bool
  }

instance Default ExamsTableFilterProj where
  def = ExamsTableFilterProj
    { etProjFilterMayAccess  = Nothing
    , etProjFilterHasResults = Nothing
    , etProjFilterLabel      = Nothing
    , etProjFilterIsSynced   = Nothing
    }

makeLenses_ ''ExamsTableFilterProj


type ExamsTableExpr = ( (               E.SqlExpr (Maybe (Entity Exam  ))
                          `E.InnerJoin` E.SqlExpr (Maybe (Entity Course))
                          `E.InnerJoin` E.SqlExpr (Maybe (Entity School))
                        )
                        `E.LeftOuterJoin`
                        (               E.SqlExpr (Maybe (Entity ExamOfficeExamLabel))
                          `E.InnerJoin` E.SqlExpr (Maybe (Entity ExamOfficeLabel))
                        )
                      )
    `E.FullOuterJoin` ( E.SqlExpr (Maybe (Entity ExternalExam))
                        `E.LeftOuterJoin`
                        (               E.SqlExpr (Maybe (Entity ExamOfficeExternalExamLabel))
                          `E.InnerJoin` E.SqlExpr (Maybe (Entity ExamOfficeLabel))
                        )
                      )

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

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

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

queryExamLabel :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExamOfficeExamLabel)))
queryExamLabel = to $ $(E.sqlIJproj 2 1) . $(E.sqlLOJproj 2 2) . $(E.sqlFOJproj 2 1)

queryLabelExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExamOfficeLabel)))
queryLabelExam = to $ $(E.sqlIJproj 2 2) . $(E.sqlLOJproj 2 2) . $(E.sqlFOJproj 2 1)

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

queryExternalExamLabel :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExamOfficeExternalExamLabel)))
queryExternalExamLabel = to $ $(E.sqlIJproj 2 1) . $(E.sqlLOJproj 2 2) . $(E.sqlFOJproj 2 2)

queryLabelExternalExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExamOfficeLabel)))
queryLabelExternalExam = to $ $(E.sqlIJproj 2 2) . $(E.sqlLOJproj 2 2) . $(E.sqlFOJproj 2 2)

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

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

resultSchool :: Traversal' ExamsTableData (Entity School)
resultSchool = _dbrOutput . _1 . _Right . _3

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

resultLabel :: Traversal' ExamsTableData (Maybe (Entity ExamOfficeLabel))
resultLabel = _dbrOutput . _1 . choosing _2 _4

resultSynchronised, resultResults :: Lens' ExamsTableData (Maybe 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 their function as exam-office) access users grades
getEOExamsR, postEOExamsR :: Handler Html
getEOExamsR = postEOExamsR
postEOExamsR = do
  (uid, User{..}) <- requireAuthPair
  now <- liftIO getCurrentTime
  mr <- getMessageRender

  getSynced <- lookupGetParam "synced" <&>
    (\case
      Just "yes" -> True
      Just "no"  -> False
      _          -> userExamOfficeGetSynced
    )

  getLabels <- lookupGetParam "labels" <&>
    (\case
      Just "yes" -> True
      Just "no"  -> False
      _          -> userExamOfficeGetLabels
    )

  (examsRes, examsTable) <- runDB $ do
    let labelFilterNoLabelOption = Option
          { optionDisplay        = mr MsgExamOfficeExamsNoLabel
          , optionInternalValue  = Nothing
          , optionExternalValue  = "no-label"
          }
    labelFilterOptions <- mkOptionList . (labelFilterNoLabelOption :) <$> do
      labels <- E.select . E.from $ \examOfficeLabel -> do
        E.where_ $ examOfficeLabel E.^. ExamOfficeLabelUser E.==. E.val uid
        E.orderBy [ E.asc $ examOfficeLabel E.^. ExamOfficeLabelName ]
        return examOfficeLabel
      return . flip map labels $ \(Entity lblId ExamOfficeLabel{..})
        -> Option { optionDisplay       = examOfficeLabelName
                  , optionInternalValue = Just lblId
                  , optionExternalValue = examOfficeLabelName
                  }

    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

      examActions :: Map ExamAction (AForm Handler ExamActionData)
      examActions = Map.fromList $
          bool mempty
        [ ( ExamSetLabel, ExamSetLabelData
            <$> apopt (selectField' Nothing . fmap (fmap entityKey) $ optionsPersist [ExamOfficeLabelUser ==. uid] [Asc ExamOfficeLabelName] examOfficeLabelName) (fslI MsgExamLabel) Nothing
          )
        , ( ExamRemoveLabel, pure ExamRemoveLabelData )
        ] getLabels

      examsDBTable = DBTable{..}
        where
          dbtSQLQuery = runReaderT $ do
            exam               <- view queryExam
            course             <- view queryCourse
            school             <- view querySchool
            mExamLabel         <- view queryExamLabel
            mLabelExam         <- view queryLabelExam
            externalExam       <- view queryExternalExam
            mExternalExamLabel <- view queryExternalExamLabel
            mLabelExternalExam <- view queryLabelExternalExam

            lift $ do
              E.on $ mExternalExamLabel E.?. ExamOfficeExternalExamLabelLabel E.==. mLabelExternalExam E.?. ExamOfficeLabelId
              E.on $ E.maybe E.true (\externalExamLabelExternalExamId ->
                           externalExam E.?. ExternalExamId                   E.==.                  E.just externalExamLabelExternalExamId
                     )                                                             (mExternalExamLabel E.?. ExamOfficeExternalExamLabelExternalExam)
              E.on   E.false
              E.on $         mExamLabel E.?. ExamOfficeExamLabelLabel         E.==.         mLabelExam E.?. ExamOfficeLabelId
              E.on $ E.maybe E.true (\examLabelExamId ->
                                   exam E.?. ExamId                           E.==.                  E.just examLabelExamId
                     )                                                                     (mExamLabel E.?. ExamOfficeExamLabelExam)
              E.on $             course E.?. CourseSchool                     E.==.             school E.?. SchoolId
              E.on $               exam E.?. ExamCourse                       E.==.             course E.?. CourseId

              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))
              E.where_ $ E.val (not getLabels) E.||. (
                         E.val getLabels
                   E.&&. E.maybe E.true (\labelExamUser ->
                                                            labelExamUser  E.==. E.val uid
                         )         (mLabelExam E.?. ExamOfficeLabelUser)
                   E.&&. E.maybe E.true (\labelExternalExamUser ->
                                                    labelExternalExamUser  E.==. E.val uid
                         ) (mLabelExternalExam E.?. ExamOfficeLabelUser)
                   )

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

          dbtProj :: _ ExamsTableData
          dbtProj = (views _dbtProjRow . set _dbrOutput) =<< do
            exam               <- view $ _dbtProjRow . _dbrOutput . _1
            course             <- view $ _dbtProjRow . _dbrOutput . _2
            school             <- view $ _dbtProjRow . _dbrOutput . _3
            mExamLabel         <- view $ _dbtProjRow . _dbrOutput . _4
            externalExam       <- view $ _dbtProjRow . _dbrOutput . _5
            mExternalExamLabel <- view $ _dbtProjRow . _dbrOutput . _6

            forMM_ (view $ _dbtProjFilter . _etProjFilterMayAccess) $ \b -> if
              | Just (Entity _ exam') <- exam
              , Just (Entity _ course') <- course
                -> guardM . lift . lift . fmap (== b) . hasReadAccessTo . urlRoute $ examLink course' exam'
              | Just (Entity _ eexam) <- externalExam
                -> guardM . lift . lift . fmap (== b) . hasReadAccessTo . urlRoute $ externalExamLink eexam
              | otherwise
                -> error "Got neither exam nor externalExam in result"

            let
              getExamResults = for_ exam $ \(Entity examId _) -> E.selectSource . E.from $ \examResult -> do
                E.where_ $ examResult E.^. ExamResultExam E.==. E.val examId
                E.where_ $ Exam.examOfficeExamResultAuth (E.val uid) examResult
                return $ Exam.resultIsSynced (E.val uid) examResult
              getExternalExamResults = for_ externalExam $ \(Entity externalExamId _) -> E.selectSource . E.from $ \externalExamResult -> do
                E.where_ $ externalExamResult E.^. ExternalExamResultExam E.==. E.val externalExamId
                E.where_ $ ExternalExam.examOfficeExternalExamResultAuth (E.val uid) externalExamResult
                return $ ExternalExam.resultIsSynced (E.val uid) externalExamResult
              getResults = getExamResults >> getExternalExamResults
              foldResult (E.Value isSynced) = (Sum 1, guardMonoid isSynced $ Sum 1)
            
            mCounts <- if getSynced
              then do
                (Sum resCount, Sum synCount) <- lift . lift . runConduit $ getResults .| C.foldMap foldResult
                forMM_ (view $ _dbtProjFilter . _etProjFilterHasResults) $ \b ->
                  guard $ b == (resCount > 0)
                forMM_ (view $ _dbtProjFilter . _etProjFilterIsSynced) $ \b ->
                  guard $ b == (synCount >= resCount)
                return $ Just (resCount, synCount)
              else do
                forMM_ (view $ _dbtProjFilter . _etProjFilterHasResults) guard
                return Nothing

            case (exam, course, school, mExamLabel, externalExam, mExternalExamLabel) of
              (Just exam', Just course', Just school', mExamLabel', Nothing, Nothing) -> return
                (Right (exam', course', school', mExamLabel'), snd <$> mCounts, fst <$> mCounts)
              (Nothing, Nothing, Nothing, Nothing, Just externalExam', mExternalExamLabel') -> return
                (Left (externalExam', mExternalExamLabel'), snd <$> mCounts, fst <$> mCounts)
              _other -> return $ error "Got exam & externalExam in same result"


          colLabel = Colonnade.singleton (fromSortable . Sortable (Just "label") $ i18nCell MsgTableExamLabel) $ \x -> flip runReader x $ do
            mLabel <- preview resultLabel
            
            -- TODO: use select frontend util
            if
              | Just (Just (Entity _ ExamOfficeLabel{..})) <- mLabel
              -> return $ cell $(widgetFile "widgets/exam-office-label")
              | otherwise -> return $ cell mempty

          colSynced = Colonnade.singleton (fromSortable . Sortable (Just "synced") $ i18nCell MsgExamSynchronised) $ \x -> flip runReader x $ do
            mExam    <- preview resultExam
            mSchool  <- preview resultSchool
            mSynced  <- view resultSynchronised
            mResults <- view resultResults

            if
              | Just (Entity _ Exam{examClosed, examFinished}) <- mExam
              , Just (Entity _ School{schoolExamCloseMode}) <- mSchool
              , bool ((min `on` NTop) examClosed examFinished > NTop (Just now))
                     (NTop examClosed > NTop (Just now))
                   $ is _ExamCloseSeparate schoolExamCloseMode
                -> return . cell $ toWidget iconNew
              | Just synced  <- mSynced
              , Just results <- mResults
                -> do
                  isSynced <- view resultIsSynced
                  return $ cell
                     [whamlet|
                       $newline never
                       $if isSynced
                         #{iconOK}
                       $else
                         #{synced}/#{results}
                     |]
                     & cellAttrs <>~ [ ("class", "heated")
                                     , ("style", [st|--hotness: #{tshow (heat results synced)}|])
                                     ]
              | otherwise -> return $ cell mempty


          dbtColonnade :: Colonnade Sortable _ _
          dbtColonnade = mconcat
            [ bool mempty (dbSelect (applying _2) id $ \DBRow{ dbrOutput=(ex,_,_) } -> return $ bimap (\(Entity eeId _,_) -> eeId) (\(Entity eId _,_,_,_) -> eId) ex) (not $ Map.null examActions)
            , bool mempty colLabel  getLabels
            , bool mempty colSynced getSynced
            , 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 $
              bool mempty
            [ singletonMap "label-prio" $
                SortProjected . comparing $ (fmap . fmap $ examOfficeLabelPriority . entityVal) <$> preview resultLabel
            , singletonMap "label-status" $
                SortProjected . comparing $ (fmap . fmap $ examOfficeLabelStatus . entityVal) <$> preview resultLabel
            ] getLabels <>
              bool mempty
            [ singletonMap "synced" $
                SortProjected . comparing $ ((/) `on` toRational . fromMaybe 0) <$> view resultSynchronised <*> view resultResults
            , singletonMap "is-synced" $
                SortProjected . comparing $ (>=) <$> view resultSynchronised <*> view resultResults
            ] getSynced <>
            [ 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 $ (_etProjFilterMayAccess  ?~) . getAny
            , singletonMap "has-results" . FilterProjected $ (_etProjFilterHasResults ?~) . getAny
            , singletonMap "is-synced"   . FilterProjected $ (_etProjFilterIsSynced   ?~) . getAny
            , singletonMap "label"       . FilterColumn . E.mkExactFilter $ views queryLabelExam (E.?. ExamOfficeLabelId)
            ]
          dbtFilterUI mPrev = mconcat $
            [ prismAForm (singletonFilter "label"     . maybePrism _PathPiece) mPrev $ aopt (selectField' (Just $ SomeMessage MsgTableNoFilter) $ return labelFilterOptions) (fslI MsgExamLabel)
            | getLabels ] <>
            [ prismAForm (singletonFilter "is-synced" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgExamSynchronised)
            | getSynced ]

          dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }

          dbtParams = DBParamsForm
            { dbParamsFormMethod = POST
            , dbParamsFormAction = Just . SomeRoute $ ExamOfficeR EOExamsR
            , dbParamsFormAttrs  = []
            , dbParamsFormSubmit = FormSubmit
            , dbParamsFormAdditional
                = renderAForm FormStandard
                    $  (, mempty) . First . Just
                   <$> multiActionA examActions (fslI MsgTableAction) Nothing
            , dbParamsFormEvaluate = liftHandler . runFormPost
            , dbParamsFormResult   = id
            , dbParamsFormIdent    = def
            }

          dbtIdent :: Text
          dbtIdent = "exams"

          dbtCsvEncode = noCsvEncode
          dbtCsvDecode = Nothing

          dbtExtraReps = []

      examsDBTableValidator = def
        & defaultSorting (bool mempty [SortDescBy "label-prio", SortAscBy "label-status"] getLabels <> bool mempty [SortAscBy "is-synced"] getSynced <> [SortAscBy "exam-time"])
        & forceFilter "may-access" (Any True)
        & forceFilter "has-results" (Any True)

      postprocess :: FormResult (First ExamActionData , DBFormResult (Either ExternalExamId ExamId) Bool (DBRow (Either (Entity ExternalExam, Maybe (Entity ExamOfficeLabel)) (Entity Exam, Entity Course, Entity School, Maybe (Entity ExamOfficeLabel)), Maybe Natural, Maybe Natural)))
                  -> FormResult (      ExamActionData ,          Set (Either ExternalExamId ExamId))
      postprocess (FormFailure errs)  = FormFailure errs
      postprocess  FormMissing        = FormMissing
      postprocess (FormSuccess (First mExamActionData, examRes))
        | Just act <- mExamActionData = FormSuccess . (act,) . Map.keysSet . Map.filter id $ getDBFormResult (const False) examRes
        | otherwise                   = FormMissing

    over _1 postprocess <$> dbTable examsDBTableValidator examsDBTable

  formResult examsRes $ \(examAction, exams) -> case examAction of
    ExamSetLabelData{..} -> do
      runDB . forM_ (Set.toList exams) $ either (\eeid -> void $ upsert (ExamOfficeExternalExamLabel eeid easlNewLabel) [ExamOfficeExternalExamLabelLabel =. easlNewLabel]) (\eid -> void $ upsert (ExamOfficeExamLabel eid easlNewLabel) [ExamOfficeExamLabelLabel =. easlNewLabel])
      addMessageI Success $ MsgExamLabelsSet (Set.size exams)
      redirect $ ExamOfficeR EOExamsR
    ExamRemoveLabelData -> do
      runDB . forM_ (Set.toList exams) $ either
        (\eeId -> E.delete . E.from $ \extExLabel -> E.where_ (extExLabel E.^. ExamOfficeExternalExamLabelExternalExam E.==. E.val eeId))
        (\eId  -> E.delete . E.from $ \exLabel -> E.where_ (exLabel E.^. ExamOfficeExamLabelExam E.==. E.val eId))
      addMessageI Success $ MsgExamLabelsRemoved (Set.size exams)
      redirect $ ExamOfficeR EOExamsR

  siteLayoutMsg MsgHeadingExamList $ do
    setTitleI MsgHeadingExamList
    examsTable
