{-# OPTIONS_GHC -fno-warn-orphans #-}

module Handler.Course.Users
  ( queryUser
  , makeCourseUserTable
  , postCUsersR, getCUsersR
  , colUserDegreeShort, colUserField, colUserSemester
  ) where

import Import

import Utils.Form
import Handler.Utils
import Handler.Utils.Database
import Handler.Utils.Table.Cells
import Handler.Utils.Table.Columns
import Database.Persist.Sql (deleteWhereCount)
import qualified Database.Esqueleto.Utils as E
import Database.Esqueleto.Utils.TH

import Data.Function ((&))

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

import qualified Database.Esqueleto as E


type UserTableExpr   = (E.SqlExpr (Entity User) `E.InnerJoin` E.SqlExpr (Entity CourseParticipant))
        `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity CourseUserNote))
        `E.LeftOuterJoin`
        (E.SqlExpr (Maybe (Entity StudyFeatures)) `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyDegree)) `E.InnerJoin`E.SqlExpr (Maybe (Entity StudyTerms)))

-- forceUserTableType :: (UserTableExpr -> a) -> (UserTableExpr -> a)
-- forceUserTableType = id

-- Sql-Getters for this query, used for sorting and filtering (cannot be lenses due to being Esqueleto expressions)
-- This ought to ease refactoring the query
queryUser :: UserTableExpr -> E.SqlExpr (Entity User)
queryUser = $(sqlIJproj 2 1) . $(sqlLOJproj 3 1)

queryParticipant :: UserTableExpr -> E.SqlExpr (Entity CourseParticipant)
queryParticipant = $(sqlIJproj 2 2) . $(sqlLOJproj 3 1)

queryUserNote ::  UserTableExpr -> E.SqlExpr (Maybe (Entity CourseUserNote))
queryUserNote = $(sqlLOJproj 3 2)

queryFeaturesStudy ::  UserTableExpr -> E.SqlExpr (Maybe (Entity StudyFeatures))
queryFeaturesStudy  = $(sqlIJproj 3 1) . $(sqlLOJproj 3 3)

queryFeaturesDegree :: UserTableExpr -> E.SqlExpr (Maybe (Entity StudyDegree))
queryFeaturesDegree = $(sqlIJproj 3 2) . $(sqlLOJproj 3 3)

queryFeaturesField :: UserTableExpr -> E.SqlExpr (Maybe (Entity StudyTerms))
queryFeaturesField  = $(sqlIJproj 3 3) . $(sqlLOJproj 3 3)


userTableQuery :: CourseId -> UserTableExpr -> E.SqlQuery ( E.SqlExpr (Entity User)
                                                          , E.SqlExpr (E.Value UTCTime)
                                                          , E.SqlExpr (E.Value (Maybe (Key CourseUserNote)))
                                                          , StudyFeaturesDescription')
userTableQuery cid ((user `E.InnerJoin` participant) `E.LeftOuterJoin` note `E.LeftOuterJoin` studyFeatures) = do
  -- Note that order of E.on for nested joins is seemingly right-to-left, ignoring nesting paranthesis
  features <- studyFeaturesQuery' (participant E.^. CourseParticipantField) studyFeatures
  E.on $  (note E.?. CourseUserNoteUser   E.==. E.just (participant E.^. CourseParticipantUser))
    E.&&. (note E.?. CourseUserNoteCourse E.==. E.just (E.val cid))
  E.on $     participant E.^. CourseParticipantUser   E.==. user E.^. UserId
  E.where_ $ participant E.^. CourseParticipantCourse E.==. E.val cid
  return (user, participant E.^. CourseParticipantRegistration, note E.?. CourseUserNoteId, features)


type UserTableData   = DBRow (Entity User, UTCTime, Maybe CourseUserNoteId, (Maybe StudyFeatures, Maybe StudyDegree, Maybe StudyTerms))

instance HasEntity UserTableData User where
  hasEntity = _dbrOutput . _1

instance HasUser UserTableData where
  -- hasUser = _entityVal
  hasUser = _dbrOutput . _1 . _entityVal

_userTableRegistration :: Lens' UserTableData UTCTime
_userTableRegistration = _dbrOutput . _2

_userTableNote :: Lens' UserTableData (Maybe CourseUserNoteId)
_userTableNote = _dbrOutput . _3

_userTableFeatures :: Lens' UserTableData (Maybe StudyFeatures, Maybe StudyDegree, Maybe StudyTerms)
_userTableFeatures = _dbrOutput . _4

_rowUserSemester :: Traversal' UserTableData Int
_rowUserSemester = _userTableFeatures . _1 . _Just . _studyFeaturesSemester


colUserComment :: IsDBTable m c => TermId -> SchoolId -> CourseShorthand -> Colonnade Sortable UserTableData (DBCell m c)
colUserComment tid ssh csh =
    sortable (Just "note") (i18nCell MsgCourseUserNote)
      $ \DBRow{ dbrOutput=(Entity uid _, _, mbNoteKey,_) } ->
        maybeEmpty mbNoteKey $ const $
          anchorCellM (courseLink <$> encrypt uid) (hasComment True)
  where
    courseLink = CourseR tid ssh csh . CUserR

colUserSemester :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c)
colUserSemester = sortable (Just "semesternr") (i18nCell MsgStudyFeatureAge) $
    foldMap numCell . preview _rowUserSemester

colUserField :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c)
colUserField = sortable (Just "field") (i18nCell MsgCourseStudyFeature) $
    foldMap i18nCell . view (_userTableFeatures . _3)

-- colUserFieldShort :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c)
-- colUserFieldShort = sortable (Just "field-short") (i18nCell MsgCourseStudyFeature) $
--     foldMap (i18nCell . ShortStudyTerms) . view (_userTableFeatures . _3)

-- colUserDegree :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c)
-- colUserDegree = sortable (Just "degree") (i18nCell MsgStudyFeatureDegree) $
--     foldMap i18nCell . preview (_userTableFeatures . _2 . _Just)

colUserDegreeShort :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c)
colUserDegreeShort = sortable (Just "degree-short") (i18nCell MsgStudyFeatureDegree) $
    foldMap (i18nCell . ShortStudyDegree) . preview (_userTableFeatures . _2 . _Just)


data CourseUserAction = CourseUserSendMail | CourseUserDeregister
  deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)

instance Universe CourseUserAction
instance Finite CourseUserAction
nullaryPathPiece ''CourseUserAction $ camelToPathPiece' 2
embedRenderMessage ''UniWorX ''CourseUserAction id


makeCourseUserTable :: forall h act.
                       ( Functor h, ToSortable h
                       , RenderMessage UniWorX act, Eq act, PathPiece act, Finite act)
                    => CourseId
                    -> (UserTableExpr -> E.SqlExpr (E.Value Bool))
                    -> Colonnade h UserTableData (DBCell (MForm Handler) (FormResult (First act, DBFormResult UserId Bool UserTableData)))
                    -> PSValidator (MForm Handler) (FormResult (First act, DBFormResult UserId Bool UserTableData))
                    -> DB (FormResult (act, Set UserId), Widget)
makeCourseUserTable cid restrict colChoices psValidator = do
    Just currentRoute <- liftHandlerT getCurrentRoute
    -- -- psValidator has default sorting and filtering
    let dbtIdent     = "courseUsers" :: Text
        dbtStyle     = def { dbsFilterLayout = defaultDBSFilterLayout }
        dbtSQLQuery q = userTableQuery cid q <* E.where_ (restrict q)
        dbtRowKey    = queryUser >>> (E.^. UserId)
        dbtProj      = traverse $ \(user, E.Value registrationTime , E.Value userNoteId, (feature,degree,terms)) -> return (user, registrationTime, userNoteId, (entityVal <$> feature, entityVal <$> degree, entityVal <$> terms))
        dbtColonnade = colChoices
        dbtSorting   = Map.fromList
          [ sortUserNameLink    queryUser -- slower sorting through clicking name column header
          , sortUserSurname     queryUser -- needed for initial sorting
          , sortUserDisplayName queryUser -- needed for initial sorting
          , sortUserEmail       queryUser
          , sortUserMatriclenr  queryUser
          , ("degree"      , SortColumn $ queryFeaturesDegree >>> (E.?. StudyDegreeName))
          , ("degree-short", SortColumn $ queryFeaturesDegree >>> (E.?. StudyDegreeShorthand))
          , ("field"       , SortColumn $ queryFeaturesField  >>> (E.?. StudyTermsName))
          , ("field-short" , SortColumn $ queryFeaturesField  >>> (E.?. StudyTermsShorthand))
          , ("semesternr"  , SortColumn $ queryFeaturesStudy  >>> (E.?. StudyFeaturesSemester))
          , ("registration", SortColumn $ queryParticipant    >>> (E.^. CourseParticipantRegistration))
          , ("note"        , SortColumn $ queryUserNote >>> \note -> -- sort by last edit date
                E.sub_select . E.from $ \edit -> do
                  E.where_ $ note E.?. CourseUserNoteId E.==. E.just (edit E.^. CourseUserNoteEditNote)
                  return . E.max_ $ edit E.^. CourseUserNoteEditTime
            )
          ]
        dbtFilter    = Map.fromList
          [ fltrUserNameLink    queryUser
          , fltrUserEmail       queryUser
          , fltrUserMatriclenr  queryUser
          , fltrUserNameEmail   queryUser
          , ("field-name"  , FilterColumn $ E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsName))
          , ("field-short" , FilterColumn $ E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsShorthand))
          , ("field-key"   , FilterColumn $ E.mkExactFilter    $ queryFeaturesField >>> (E.?. StudyTermsKey))
          , ("field"       , FilterColumn $ E.anyFilter
                                 [ E.mkContainsFilterWith Just $ queryFeaturesField >>> E.joinV . (E.?. StudyTermsName)
                                 , E.mkContainsFilterWith Just $ queryFeaturesField >>> E.joinV . (E.?. StudyTermsShorthand)
                                 , E.mkExactFilterWith readMay $ queryFeaturesField >>> (E.?. StudyTermsKey)
                                 ] )
          , ("degree"      , FilterColumn $ E.anyFilter
                                 [ E.mkContainsFilterWith Just $ queryFeaturesDegree >>> E.joinV . (E.?. StudyDegreeName)
                                 , E.mkContainsFilterWith Just $ queryFeaturesDegree >>> E.joinV . (E.?. StudyDegreeShorthand)
                                 , E.mkExactFilterWith readMay $ queryFeaturesDegree >>> (E.?. StudyDegreeKey)
                                 ] )
          , ("semesternr"  , FilterColumn $ E.mkExactFilter    $ queryFeaturesStudy >>> (E.?. StudyFeaturesSemester))
          , ("tutorial"    , FilterColumn $ E.mkExistsFilter   $ \row criterion ->
                E.from $ \(tutorial `E.InnerJoin` tutorialParticipant) -> do
                  E.on $ tutorial E.^. TutorialId E.==. tutorialParticipant E.^. TutorialParticipantTutorial
                  E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid
                       E.&&. E.hasInfix (tutorial E.^. TutorialName) (E.val criterion :: E.SqlExpr (E.Value (CI Text)))
                       E.&&. tutorialParticipant E.^. TutorialParticipantUser E.==. queryUser row E.^. UserId
            )
          -- , ("course-registration", error "TODO") -- TODO
          -- , ("course-user-note", error "TODO") -- TODO
          ]
        dbtFilterUI mPrev = mconcat
          [ fltrUserNameEmailUI  mPrev
          , fltrUserMatriclenrUI mPrev
          , prismAForm (singletonFilter "degree")   mPrev $ aopt textField (fslI MsgStudyFeatureDegree)
          , prismAForm (singletonFilter "field")    mPrev $ aopt textField (fslI MsgCourseStudyFeature)
          , prismAForm (singletonFilter "tutorial") mPrev $ aopt textField (fslI MsgCourseTutorial)
          ]
        dbtParams    = DBParamsForm
          { dbParamsFormMethod = POST
          , dbParamsFormAction = Just $ SomeRoute currentRoute
          , dbParamsFormAttrs  = []
          , dbParamsFormSubmit = FormSubmit
          , dbParamsFormAdditional
              = renderAForm FormStandard
                   $  (, mempty) . First . Just
                  <$> areq (selectField optionsFinite) (fslI MsgAction) Nothing
          , dbParamsFormEvaluate = liftHandlerT . runFormPost
          , dbParamsFormResult   = id
          , dbParamsFormIdent    = def
          }
        dbtCsvEncode = noCsvEncode
        dbtCsvDecode = Nothing
    over _1 postprocess <$> dbTable psValidator DBTable{..}
  where
    postprocess :: FormResult (First act, DBFormResult UserId Bool UserTableData) -> FormResult (act, Set UserId)
    postprocess inp = do
      (First (Just act), usrMap) <- inp
      let usrSet = Map.keysSet . Map.filter id $ getDBFormResult (const False) usrMap
      return (act, usrSet)

getCUsersR, postCUsersR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCUsersR = postCUsersR
postCUsersR tid ssh csh = do
  (Entity cid course, numParticipants, (participantRes,participantTable)) <- runDB $ do
    let colChoices  = mconcat
          [ dbSelect (applying _2) id (return . view (hasEntity . _entityKey))
          , colUserNameLink (CourseR tid ssh csh . CUserR)
          , colUserEmail
          , colUserMatriclenr
          , colUserDegreeShort
          , colUserField
          , colUserSemester
          , sortable (Just "registration") (i18nCell MsgRegisteredSince) (dateCell . view _userTableRegistration)
          , colUserComment tid ssh csh
          ]
        psValidator = def & defaultSortingByName
    ent@(Entity cid _) <- getBy404 $ TermSchoolCourseShort tid ssh csh
    numParticipants <- count [CourseParticipantCourse ==. cid]
    table <- makeCourseUserTable cid (const E.true) colChoices psValidator
    return (ent, numParticipants, table)
  formResult participantRes $ \case
    (CourseUserSendMail, selectedUsers) -> do
        cids <- traverse encrypt $ Set.toList selectedUsers :: Handler [CryptoUUIDUser]
        redirect (CourseR tid ssh csh CCommR, [(toPathPiece GetRecipient, toPathPiece cID) | cID <- cids])
    (CourseUserDeregister,selectedUsers) -> do
        Sum nrDel <- fmap mconcat . runDB . forM (Set.toList selectedUsers) $ \uid -> do
          nrDel <- deleteWhereCount
            [ CourseParticipantCourse ==. cid
            , CourseParticipantUser ==. uid
            ]
          unless (nrDel == 0) $
            audit $ TransactionCourseParticipantDeleted cid uid
          return $ Sum nrDel
        addMessageI Success $ MsgCourseUsersDeregistered nrDel
        redirect $ CourseR tid ssh csh CUsersR
  let headingLong  = [whamlet|_{MsgMenuCourseMembers} #{courseName course} #{tid}|]
      headingShort = prependCourseTitle tid ssh csh MsgCourseMembers
  siteLayout headingLong $ do
    setTitleI headingShort
    $(widgetFile "course-participants")
