module Handler.Utils.StudyFeatures
  ( module Handler.Utils.StudyFeatures.Parse
  , UserTableStudyFeature(..)
  , _userTableField, _userTableDegree, _userTableSemester, _userTableFieldType
  , UserTableStudyFeatures(..)
  , _UserTableStudyFeatures
  , isRelevantStudyFeature, isRelevantStudyFeatureCached
  , cacheStudyFeatureRelevance
  , isCourseStudyFeature, courseUserStudyFeatures
  , isExternalExamStudyFeature, externalExamUserStudyFeatures
  , isTermStudyFeature
  , isAllocationStudyFeature, allocationUserStudyFeatures
  ) where

import Import.NoFoundation
import Foundation.Type
import Foundation.I18n

import Utils.Term

import Handler.Utils.StudyFeatures.Parse

import qualified Data.Csv as Csv

import qualified Data.Set as Set

import Data.RFC5051 (compareUnicode)

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

import qualified Data.Conduit.Combinators as C


data UserTableStudyFeature = UserTableStudyFeature
  { userTableField
  , userTableDegree    :: Text
  , userTableSemester  :: Int
  , userTableFieldType :: StudyFieldType
  } deriving (Eq, Ord, Read, Show, Generic, Typeable)
makeLenses_ ''UserTableStudyFeature

deriveJSON defaultOptions
  { fieldLabelModifier = camelToPathPiece' 2
  } ''UserTableStudyFeature

newtype UserTableStudyFeatures = UserTableStudyFeatures (Set UserTableStudyFeature)
  deriving (Eq, Ord, Read, Show, Generic, Typeable)
  deriving newtype ( ToJSON, FromJSON
                   , Semigroup, Monoid
                   )
makeWrapped ''UserTableStudyFeatures

_UserTableStudyFeatures :: Iso' UserTableStudyFeatures [UserTableStudyFeature]
_UserTableStudyFeatures = iso (sortBy userTableStudyFeatureSort . Set.toList . view _Wrapped) (UserTableStudyFeatures . Set.fromList)

instance Csv.ToField UserTableStudyFeature where
  toField UserTableStudyFeature{..} = encodeUtf8
    [st|#{userTableField} #{userTableDegree} (#{userTableFieldType'} #{tshow userTableSemester})|]
    where userTableFieldType' = renderMessage
            (error "Foundation inspected during renderMessage" :: UniWorX)
            [] $ ShortStudyFieldType userTableFieldType

instance Csv.ToField UserTableStudyFeatures where
  toField = Csv.toField . CsvSemicolonList . view _UserTableStudyFeatures

userTableStudyFeatureSort :: UserTableStudyFeature
                          -> UserTableStudyFeature
                          -> Ordering
userTableStudyFeatureSort = mconcat
  [ compareUnicode `on` userTableDegree
  , comparing userTableSemester
  , comparing userTableFieldType
  , compareUnicode `on` userTableField
  ]


isRelevantStudyFeature :: PersistEntity record
                       => E.SqlExpr (E.Value UTCTime)
                       -> EntityField record TermId
                       -> E.SqlExpr (Entity record)
                       -> E.SqlExpr (Entity StudyFeatures)
                       -> E.SqlExpr (E.Value Bool)
isRelevantStudyFeature now termField record studyFeatures
      = (       (       overlap studyFeatures E.>. E.val 0
                  E.||. (       E.just (studyFeatures E.^. StudyFeaturesLastObserved) E.==. studyFeatures E.^. StudyFeaturesFirstObserved
                          E.&&. termStart E.<=. E.day (studyFeatures E.^. StudyFeaturesLastObserved)
                          E.&&. E.day (studyFeatures E.^. StudyFeaturesLastObserved) E.<=. termEnd
                        )
                )
          E.&&. E.not_ (E.exists betterOverlap)
        )
  E.||. ( termIsActiveE now E.nothing (record E.^. termField)
          E.&&. E.not_ (E.exists anyOverlap)
          E.&&. studyFeatures E.^. StudyFeaturesValid
        )
  where termEnd = E.subSelectForeign record termField (E.^. TermEnd)
        termStart = E.subSelectForeign record termField (E.^. TermStart)

        overlap :: E.SqlExpr (Entity StudyFeatures) -> E.SqlExpr (E.Value Int)
        overlap studyFeatures'
                     = E.min (E.day $ studyFeatures' E.^. StudyFeaturesLastObserved) termEnd
          `E.diffDays` E.maybe termStart (E.max termStart . E.day) (studyFeatures' E.^. StudyFeaturesFirstObserved)

        anyOverlap = E.from $ \studyFeatures' -> do
          E.where_ $ studyFeatures' E.^. StudyFeaturesUser E.==. studyFeatures E.^. StudyFeaturesUser
          E.where_ $ overlap studyFeatures' E.>. E.val 0

        betterOverlap = E.from $ \studyFeatures' -> do  
          E.where_ $ studyFeatures' E.^. StudyFeaturesUser E.==. studyFeatures E.^. StudyFeaturesUser
               E.&&. studyFeatures' E.^. StudyFeaturesDegree E.==. studyFeatures E.^. StudyFeaturesDegree
               E.&&. studyFeatures' E.^. StudyFeaturesField E.==. studyFeatures E.^. StudyFeaturesField
               E.&&. studyFeatures' E.^. StudyFeaturesSuperField `E.maybeEq` studyFeatures E.^. StudyFeaturesSuperField
               E.&&. studyFeatures' E.^. StudyFeaturesType E.==. studyFeatures E.^. StudyFeaturesType
          E.where_ $ E.abs (studyFeatures' E.^. StudyFeaturesSemester E.-. studyFeatures E.^. StudyFeaturesSemester) E.==. E.val 1
               E.&&. overlap studyFeatures' E.>. overlap studyFeatures

isRelevantStudyFeatureCached :: PersistEntity record
                              => EntityField record TermId
                              -> E.SqlExpr (Entity record)
                              -> E.SqlExpr (Entity StudyFeatures)
                              -> E.SqlExpr (E.Value Bool)
isRelevantStudyFeatureCached termField record studyFeatures
  = E.exists . E.from $ \relevantStudyFeatures ->
      E.where_ $ relevantStudyFeatures E.^. RelevantStudyFeaturesTerm E.==. record E.^. termField
           E.&&. relevantStudyFeatures E.^. RelevantStudyFeaturesStudyFeatures E.==. studyFeatures E.^. StudyFeaturesId

cacheStudyFeatureRelevance :: MonadResource m
                           => (E.SqlExpr (Entity StudyFeatures) -> E.SqlExpr (E.Value Bool))
                           -> SqlPersistT m ()
cacheStudyFeatureRelevance fFilter = do
  now <- liftIO getCurrentTime

  E.insertSelectWithConflict UniqueRelevantStudyFeatures
    ( E.from $ \(studyFeatures `E.InnerJoin` term) -> do
        E.on E.true
        E.where_ $ fFilter studyFeatures
        E.where_ $ isRelevantStudyFeature (E.val now) TermId term studyFeatures
        return $ RelevantStudyFeatures E.<# (term E.^. TermId) E.<&> (studyFeatures E.^. StudyFeaturesId)
    )
    ( \_current _excluded -> [] )

  let getStudyFeatures = E.selectSource . E.from $ \studyFeatures -> do
        E.where_ $ fFilter studyFeatures
        E.where_ . E.isNothing $ studyFeatures E.^. StudyFeaturesRelevanceCached
        return $ studyFeatures E.^. StudyFeaturesId
      migrateStudyFeatures genUUID lift' (E.Value sfId) = do
        uuid <- genUUID
        lift' $ update sfId [ StudyFeaturesRelevanceCached =. Just uuid ]
   in runConduit $ getStudyFeatures .| randUUIDC (\genUUID lift' -> C.mapM_ $ migrateStudyFeatures genUUID lift')

isCourseStudyFeature :: E.SqlExpr (Entity Course) -> E.SqlExpr (Entity StudyFeatures) -> E.SqlExpr (E.Value Bool)
isCourseStudyFeature = isRelevantStudyFeatureCached CourseTerm

courseUserStudyFeatures :: MonadIO m => CourseId -> UserId -> SqlPersistT m UserTableStudyFeatures
courseUserStudyFeatures cId uid = do
  feats <- E.select . E.from $ \(course `E.InnerJoin` studyFeatures `E.InnerJoin` terms `E.InnerJoin` degree) -> do
    E.on $ degree E.^. StudyDegreeId E.==. studyFeatures E.^. StudyFeaturesDegree
    E.on $ terms E.^. StudyTermsId E.==. studyFeatures E.^. StudyFeaturesField
    E.on $ isCourseStudyFeature course studyFeatures
    E.where_ $ course E.^. CourseId E.==. E.val cId
    E.where_ $ studyFeatures E.^. StudyFeaturesUser E.==. E.val uid
    return (terms, degree, studyFeatures)
  return . UserTableStudyFeatures . Set.fromList . flip map feats $
    \(Entity _ StudyTerms{..}, Entity _ StudyDegree{..}, Entity _ StudyFeatures{..}) -> UserTableStudyFeature
      { userTableField = fromMaybe (tshow studyTermsKey) studyTermsName
      , userTableDegree = fromMaybe (tshow studyDegreeKey) studyDegreeName
      , userTableSemester = studyFeaturesSemester
      , userTableFieldType = studyFeaturesType
      }

isExternalExamStudyFeature :: E.SqlExpr (Entity ExternalExam) -> E.SqlExpr (Entity StudyFeatures) -> E.SqlExpr (E.Value Bool)
isExternalExamStudyFeature = isRelevantStudyFeatureCached ExternalExamTerm

externalExamUserStudyFeatures :: MonadIO m => ExternalExamId -> UserId -> SqlPersistT m UserTableStudyFeatures
externalExamUserStudyFeatures eeId uid = do
  feats <- E.select . E.from $ \(externalExam `E.InnerJoin` studyFeatures `E.InnerJoin` terms `E.InnerJoin` degree) -> do
    E.on $ degree E.^. StudyDegreeId E.==. studyFeatures E.^. StudyFeaturesDegree
    E.on $ terms E.^. StudyTermsId E.==. studyFeatures E.^. StudyFeaturesField
    E.on $ isExternalExamStudyFeature externalExam studyFeatures
    E.where_ $ externalExam E.^. ExternalExamId E.==. E.val eeId
    E.where_ $ studyFeatures E.^. StudyFeaturesUser E.==. E.val uid
    return (terms, degree, studyFeatures)
  return . UserTableStudyFeatures . Set.fromList . flip map feats $
    \(Entity _ StudyTerms{..}, Entity _ StudyDegree{..}, Entity _ StudyFeatures{..}) -> UserTableStudyFeature
      { userTableField = fromMaybe (tshow studyTermsKey) studyTermsName
      , userTableDegree = fromMaybe (tshow studyDegreeKey) studyDegreeName
      , userTableSemester = studyFeaturesSemester
      , userTableFieldType = studyFeaturesType
      }

isTermStudyFeature :: E.SqlExpr (Entity Term) -> E.SqlExpr (Entity StudyFeatures) -> E.SqlExpr (E.Value Bool)
isTermStudyFeature = isRelevantStudyFeatureCached TermId


isAllocationStudyFeature :: E.SqlExpr (Entity Allocation) -> E.SqlExpr (Entity StudyFeatures) -> E.SqlExpr (E.Value Bool)
isAllocationStudyFeature = isRelevantStudyFeatureCached AllocationTerm
  
allocationUserStudyFeatures :: MonadIO m => AllocationId -> UserId -> SqlPersistT m UserTableStudyFeatures
allocationUserStudyFeatures aId uid = do
  feats <- E.select . E.from $ \(allocation `E.InnerJoin` studyFeatures `E.InnerJoin` terms `E.InnerJoin` degree) -> do
    E.on $ degree E.^. StudyDegreeId E.==. studyFeatures E.^. StudyFeaturesDegree
    E.on $ terms E.^. StudyTermsId E.==. studyFeatures E.^. StudyFeaturesField
    E.on $ isAllocationStudyFeature allocation studyFeatures
    E.where_ $ allocation E.^. AllocationId E.==. E.val aId
    E.where_ $ studyFeatures E.^. StudyFeaturesUser E.==. E.val uid
    return (terms, degree, studyFeatures)
  return . UserTableStudyFeatures . Set.fromList . flip map feats $
    \(Entity _ StudyTerms{..}, Entity _ StudyDegree{..}, Entity _ StudyFeatures{..}) -> UserTableStudyFeature
      { userTableField = fromMaybe (tshow studyTermsKey) studyTermsName
      , userTableDegree = fromMaybe (tshow studyDegreeKey) studyDegreeName
      , userTableSemester = studyFeaturesSemester
      , userTableFieldType = studyFeaturesType
      }
