module Handler.Utils.Allocation
  ( allocationDone
  , ordinalPriorities
  , sinkAllocationPriorities
  , computeAllocation
  , doAllocation
  , ppMatchingLog
  , storeAllocationResult
  ) where

import Import

import qualified Data.Map.Strict as Map

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

import qualified Control.Monad.State.Class as State (get, modify')

import Data.List (genericLength, elemIndex)
import qualified Data.Vector as Vector
import Data.Vector.Lens (vector)
import qualified Data.Set as Set

import System.Random (mkStdGen)

import Utils.Allocation

import qualified Data.Conduit.List as C
import Data.Conduit.Lift (evalStateC)

import Data.Generics.Product.Param

import qualified Crypto.Hash as Crypto
import qualified Data.Binary as Binary

import qualified Data.ByteArray as BA (convert)


allocationDone :: AllocationId -> DB (Maybe UTCTime)
allocationDone allocId = fmap (E.unValue <=< listToMaybe) . E.select . E.from $ \participant -> do
  E.where_ $ participant E.^. CourseParticipantAllocated E.==. E.just (E.val allocId)
  return . E.max_ $ participant E.^. CourseParticipantRegistration


ordinalPriorities :: Monad m => ConduitT UserMatriculation (Map UserMatriculation AllocationPriority) m ()
ordinalPriorities = evalStateC 0 . C.mapM $ \matr -> singletonMap matr <$> (AllocationPriorityOrdinal <$> State.get <* State.modify' succ)
  
sinkAllocationPriorities :: AllocationId
                         -> ConduitT (Map UserMatriculation AllocationPriority) Void DB ()
sinkAllocationPriorities allocId = C.mapM_ . imapM_ $ \matr prio ->
  E.update $ \allocationUser -> do
    E.set allocationUser [ AllocationUserPriority E.=. E.val (Just prio) ]
    E.where_ $ allocationUser E.^. AllocationUserAllocation E.==. E.val allocId
    E.where_ . E.exists . E.from $ \user ->
      E.where_ $ user E.^. UserId E.==. allocationUser E.^. AllocationUserUser
           E.&&. user E.^. UserMatrikelnummer E.==. E.val (Just matr)

  
computeAllocation :: AllocationId
                  -> Maybe (Set CourseId) -- ^ Optionally restrict allocation to only consider the given courses
                  -> DB ( AllocationFingerprint
                        , Set (UserId, CourseId)
                        , Seq (MatchingLog UserId CourseId Natural)
                        )
computeAllocation allocId cRestr = do
  allocations <- selectList [ CourseParticipantAllocated ==. Just allocId ] []
  let allocations' = allocations
        & map ((, Sum 1) . courseParticipantUser . entityVal)
        & Map.fromListWith (<>)
        & fmap getSum
  
  users' <- selectList [ AllocationUserAllocation ==. allocId ] []
  let users'' = users'
        & mapMaybe ( runMaybeT $ do
                       user <- lift $ allocationUserUser . entityVal
                       totalCourses <- lift $ allocationUserTotalCourses . entityVal
                       priority <- MaybeT $ allocationUserPriority . entityVal

                       let allocated = Map.findWithDefault 0 user allocations'

                       guard $ totalCourses > allocated

                       return (user, (totalCourses - allocated, priority))
                   )
        & Map.fromList
      cloneCounts = Map.map (view _1) users''
      allocationPrio = view _2 . (Map.!) users''

  courses' <- E.select . E.from $ \(allocationCourse `E.InnerJoin` course) -> do
    E.on $ allocationCourse E.^. AllocationCourseCourse E.==. course E.^. CourseId
     E.&&. allocationCourse E.^. AllocationCourseAllocation E.==. E.val allocId

    let participants = E.subSelectCount . E.from $ \participant -> do
          E.where_ $ participant E.^. CourseParticipantCourse E.==. course E.^. CourseId
          E.where_ . E.not_ . E.exists . E.from $ \lecturer ->
            E.where_ $ lecturer E.^. LecturerCourse E.==. course E.^. CourseId
                 E.&&. lecturer E.^. LecturerUser E.==. participant E.^. CourseParticipantUser

    whenIsJust cRestr $ \restrSet ->
      E.where_ $ course E.^. CourseId `E.in_` E.valList (Set.toList restrSet)

    return ( allocationCourse
           , E.maybe E.nothing (\c -> E.just $ c E.-. participants) (course E.^. CourseCapacity)
           , allocationCourse E.^. AllocationCourseMinCapacity E.-. participants
           )
  let capacities = Map.filter (maybe True (> 0)) . Map.fromList $ (view (_1 . _entityVal . _allocationCourseCourse) &&& view (_2 . _Value)) <$> courses'

  applications' <- selectList [ CourseApplicationAllocation ==. Just allocId ] []
  let alreadyAssigned (Entity _ CourseApplication{..}) = orM
        [ exists [ TransactionLogInfo ==. toJSON (TransactionCourseParticipantDeleted courseApplicationCourse courseApplicationUser) ]
        , exists [ CourseParticipantCourse ==. courseApplicationCourse, CourseParticipantUser ==. courseApplicationUser ]
        ]
  applications'' <- applications'
    & filter ((\CourseApplication{..} -> not courseApplicationRatingVeto && fromMaybe True (courseApplicationRatingPoints ^? _Just . passingGrade . _Wrapped)) . entityVal)
    & filterM (fmap not . alreadyAssigned)
  let preferences = Map.fromList $ do
        Entity _ CourseApplication{..} <- applications''
        guard $ Map.member courseApplicationCourse capacities
        return ((courseApplicationUser, courseApplicationCourse), (courseApplicationAllocationPriority, courseApplicationRatingPoints))

  gradeScale <- getsYesod $ view _appAllocationGradeScale
  gradeOrdinalPlaces <- getsYesod $ view _appAllocationGradeOrdinalPlaces
  let centralNudge user cloneIndex grade = case allocationPrio user of
        AllocationPriorityNumeric{..}
          -> let allocationPriorities' = under vector (sortOn Down) allocationPriorities
                 minPrio | Vector.null allocationPriorities' = 0
                         | otherwise = Vector.last allocationPriorities'
              in AllocationPriorityComparisonNumeric . withNumericGrade . fromInteger . fromMaybe minPrio $ allocationPriorities Vector.!? fromIntegral cloneIndex
        AllocationPriorityOrdinal{..}
          | gradeOrdinalPlaces > 0
            -> let allocationOrdinal' = gradeScale / fromIntegral gradeOrdinalPlaces * fromIntegral allocationOrdinal 
                in AllocationPriorityComparisonOrdinal (Down cloneIndex) $ withNumericGrade allocationOrdinal'
        AllocationPriorityOrdinal{..}
          -> AllocationPriorityComparisonOrdinal (Down cloneIndex) $ fromIntegral allocationOrdinal
        where
          withNumericGrade :: Rational -> Rational
          withNumericGrade
            | Just grade' <- grade
            = let numberGrade' = fromMaybe (error "non-passing grade") (fromIntegral <$> elemIndex grade' passingGrades) / pred (genericLength passingGrades)
                  passingGrades = sort $ filter (view $ passingGrade . _Wrapped) universeF
                  numericGrade = -gradeScale + numberGrade' * 2 * gradeScale
               in (+) numericGrade
            | otherwise
            = id

  let
    fingerprint :: AllocationFingerprint
    fingerprint = Crypto.hash . toStrict $ Binary.encode (users'', capacities, preferences, gradeScale, gradeOrdinalPlaces)
    
    g = mkStdGen $ hash (BA.convert fingerprint :: ByteString)

  let
    doAllocationWithout cs = runWriter $ computeMatchingLog g cloneCounts capacities' preferences' centralNudge
      where
        capacities'  = Map.filterWithKey (\    c  _ -> Set.notMember c cs) capacities
        preferences' = Map.filterWithKey (\(_, c) _ -> Set.notMember c cs) preferences

    allocationLoop cs
      | not $ null belowMin = doAllocationWithout $ cs <> Set.fromList belowMin
      | otherwise           = (allocs, mLog)
      where
        (allocs, mLog) = doAllocationWithout cs
        belowMin = catMaybes . flip map courses' $ \(Entity _ AllocationCourse{..}, _, E.Value minCap) ->
          guardOn (Set.size (Set.filter (\(_, c) -> c == allocationCourseCourse) allocs) < minCap) allocationCourseCourse

  return . (\(ms, mLog) -> (fingerprint, ms, mLog)) $!! allocationLoop Set.empty


doAllocation :: AllocationId
             -> Set (UserId, CourseId)
             -> DB ()
doAllocation allocId regs = do
  now <- liftIO getCurrentTime
  forM_ regs $ \(uid, cid) -> do
    mField <- (courseApplicationField . entityVal =<<) . listToMaybe <$> selectList [CourseApplicationCourse ==. cid, CourseApplicationUser ==. uid, CourseApplicationAllocation ==. Just allocId] []
    void . insertUnique $ CourseParticipant cid uid now mField (Just allocId)

ppMatchingLog :: ( MonoFoldable mono
                 , Element mono ~ MatchingLog UserId CourseId Natural
                 )
              => mono -> Text
ppMatchingLog = unlines . map (tshow . pretty) . otoList
  where
    pretty = over (param @1) fromSqlKey
           . over (param @2) fromSqlKey

storeAllocationResult :: AllocationId
                      -> (AllocationFingerprint, Set (UserId, CourseId), Seq (MatchingLog UserId CourseId Natural))
                      -> DB ()
storeAllocationResult allocId (allocFp, allocMatchings, ppMatchingLog -> allocLog) = do
  now <- liftIO getCurrentTime
  insert_ . AllocationMatching allocId allocFp <=< insert $ File "matchings.log" (Just $ encodeUtf8 allocLog) now

  doAllocation allocId allocMatchings
