module Handler.Allocation.Form
  ( AllocationForm(..)
  , allocationForm
  ) where

import Import

import Handler.Utils

import qualified Database.Esqueleto.Legacy as E

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

import qualified Control.Monad.State.Class as State

import qualified Data.CaseInsensitive as CI
import qualified Data.Text as Text
  

data AllocationForm = AllocationForm
  { afTerm :: TermId
  , afSchool :: SchoolId
  , afShorthand :: AllocationShorthand
  , afName :: AllocationName
  , afLegacyShorthands :: Set AllocationShorthand
  , afDescription, afStaffDescription :: Maybe StoredMarkup
  , afStaffRegisterFrom, afStaffRegisterTo
  , afRegisterFrom, afRegisterTo
  , afStaffAllocationFrom, afStaffAllocationTo
  , afRegisterByStaffFrom, afRegisterByStaffTo
  , afRegisterByCourse, afOverrideDeregister :: Maybe UTCTime
  } deriving (Eq, Ord, Read, Show, Generic, Typeable)


allocationForm :: Maybe AllocationForm
               -> AForm (YesodDB UniWorX) AllocationForm
allocationForm mTemplate = validateAForm validateAllocationForm . wFormToAForm $ do
  mayEditTerms <- lift . lift $ hasWriteAccessTo TermEditR
  now <- liftIO getCurrentTime
  muid <- maybeAuthId
  termOptions <-
    let termQuery :: E.SqlQuery (E.SqlExpr (E.Value TermId))
        termQuery = E.from $ \t -> do
          unless mayEditTerms $
            E.where_ $ E.just (t E.^. TermId) E.==. E.val (afTerm <$> mTemplate)
                 E.||. termIsActiveE (E.val now) (E.val muid) (t E.^. TermId)
          E.orderBy [E.desc $ t E.^. TermStart]
          return $ t E.^. TermId
     in lift . lift $ mkOptionsE
          termQuery
          (return . toPathPiece . E.unValue)
          (return . ShortTermIdentifier . unTermKey . E.unValue)
          (return . E.unValue)

  schoolOptions <-
    let schoolQuery :: E.SqlQuery (E.SqlExpr (Entity School))
        schoolQuery = E.from $ \s -> do
          E.where_ $ E.exists (E.from $ \userFunction -> do
                                 E.where_ $ userFunction E.^. UserFunctionSchool E.==. s E.^. SchoolId
                                      E.&&. E.just (userFunction E.^. UserFunctionUser) E.==. E.val muid
                                      E.&&. userFunction E.^. UserFunctionFunction `E.in_` E.valList [SchoolAdmin, SchoolAllocation]
                              )
               E.||. E.just (s E.^. SchoolId) E.==. E.val (afSchool <$> mTemplate)
          E.orderBy [E.asc $ s E.^. SchoolShorthand]
          return s
     in lift . lift $ mkOptionsE
          schoolQuery
          (return . toPathPiece . entityKey)
          (return . schoolName . entityVal)
          (return . entityKey)

  template <- maybe (lift . lift $ suggestAllocationForm termOptions schoolOptions) (return . Just) mTemplate

  let cfCommaSeparatedSet :: forall m. Functor m => Field m Text -> Field m (Set (CI Text))
      cfCommaSeparatedSet = guardField (not . Set.null) . convertField (Set.fromList . mapMaybe (fmap CI.mk . assertM' (not . Text.null) . Text.strip) . Text.splitOn ",") (Text.intercalate ", " . map CI.original . Set.toList)

  aFormToWForm . hoistAForm liftHandler $ AllocationForm
    <$> areq (selectField $ return termOptions) (fslI MsgAllocationFormTerm) (afTerm <$> template)
    <*> areq (selectField $ return schoolOptions) (fslI MsgAllocationFormSchool) (afSchool <$> template)
    <*> areq (textField & cfStrip & cfCI) (fslI MsgAllocationFormShorthand) (afShorthand <$> template)
    <*> areq (textField & cfStrip & cfCI) (fslI MsgAllocationFormName) (afName <$> template)
    <*> (fromMaybe Set.empty <$> aopt (textField & cfCommaSeparatedSet) (fslI MsgAllocationFormLegacyShorthands & setTooltip MsgAllocationFormLegacyShorthandsTip) (fmap Just $ afLegacyShorthands <$> template))
    <*  aformSection MsgAllocationFormDescriptions
    <*> aopt htmlField (fslI MsgAllocationFormDescription & setTooltip MsgAllocationFormDescriptionTip) (afDescription <$> template)
    <*> aopt htmlField (fslI MsgAllocationFormStaffDescription & setTooltip MsgAllocationFormStaffDescriptionTip) (afStaffDescription <$> template)
    <*  aformSection MsgAllocationFormDeadlines
    <*> aopt utcTimeField (fslI MsgAllocationFormStaffRegisterFrom & setTooltip MsgAllocationFormStaffRegisterFromTip) (afStaffRegisterFrom <$> template)
    <*> aopt utcTimeField (fslI MsgAllocationFormStaffRegisterTo & setTooltip MsgAllocationFormStaffRegisterToTip) (afStaffRegisterTo <$> template)
    <*> aopt utcTimeField (fslI MsgAllocationFormRegisterFrom & setTooltip MsgAllocationFormRegisterFromTip) (afRegisterFrom <$> template)
    <*> aopt utcTimeField (fslI MsgAllocationFormRegisterTo & setTooltip MsgAllocationFormRegisterToTip) (afRegisterTo <$> template)
    <*> aopt utcTimeField (fslI MsgAllocationFormStaffAllocationFrom & setTooltip MsgAllocationFormStaffAllocationFromTip) (afStaffAllocationFrom <$> template)
    <*> aopt utcTimeField (fslI MsgAllocationFormStaffAllocationTo & setTooltip MsgAllocationFormStaffAllocationToTip) (afStaffAllocationTo <$> template)
    <*> aopt utcTimeField (fslI MsgAllocationFormRegisterByStaffFrom & setTooltip MsgAllocationFormRegisterByStaffFromTip) (afRegisterByStaffFrom <$> template)
    <*> aopt utcTimeField (fslI MsgAllocationFormRegisterByStaffTo & setTooltip MsgAllocationFormRegisterByStaffToTip) (afRegisterByStaffTo <$> template)
    <*> aopt utcTimeField (fslI MsgAllocationFormRegisterByCourse & setTooltip MsgAllocationFormRegisterByCourseTip) (afRegisterByCourse <$> template)
    <*> aopt utcTimeField (fslI MsgAllocationFormOverrideDeregister & setTooltip MsgAllocationFormOverrideDeregisterTip) (afOverrideDeregister <$> template)

validateAllocationForm :: FormValidator AllocationForm (YesodDB UniWorX) ()
validateAllocationForm = do
  State.modify $ \af -> af { afLegacyShorthands = Set.delete (afShorthand af) $ afLegacyShorthands af }
  
  AllocationForm{..} <- State.get

  guardValidation MsgAllocationFormStaffRegisterToMustBeAfterFrom
    $ NTop afStaffRegisterFrom <= NTop afStaffRegisterTo
  guardValidation MsgAllocationFormStaffAllocationToMustBeAfterFrom
    $ NTop afStaffAllocationFrom <= NTop afStaffAllocationTo
  guardValidation MsgAllocationFormRegisterToMustBeAfterFrom
    $ NTop afRegisterFrom <= NTop afRegisterTo
  guardValidation MsgAllocationFormRegisterByStaffToMustBeAfterFrom
    $ NTop afRegisterByStaffFrom <= NTop afRegisterByStaffTo

  guardValidation MsgAllocationFormStaffRegisterFromMustBeBeforeStaffAllocationFrom
    $ NTop afStaffRegisterFrom <= NTop afStaffAllocationFrom
  guardValidation MsgAllocationFormStaffRegisterToMustBeBeforeStaffAllocationTo
    $ NTop afStaffRegisterTo <= NTop afStaffAllocationTo

  guardValidation MsgAllocationFormStaffRegisterFromMustBeBeforeRegisterFrom
    $ NTop afStaffRegisterFrom <= NTop afRegisterFrom
  guardValidation MsgAllocationFormStaffRegisterToMustBeBeforeRegisterTo
    $ NTop afStaffRegisterTo <= NTop afRegisterTo

  warnValidation MsgAllocationFormStaffAllocationToShouldBeBeforeRegisterByStaffFrom
    $ NTop afStaffAllocationTo <= NTop afRegisterByStaffFrom
  warnValidation MsgAllocationFormStaffAllocationToShouldBeBeforeRegisterByCourse
    $ NTop afStaffAllocationTo <= NTop afRegisterByCourse
  warnValidation MsgAllocationFormStaffAllocationToShouldBeAfterRegisterTo
    $ NTop afStaffAllocationTo >= NTop afRegisterTo

  warnValidation MsgAllocationFormRegisterToShouldBeBeforeRegisterByStaffFrom
    $ NTop afRegisterTo <= NTop afRegisterByStaffFrom
  warnValidation MsgAllocationFormRegisterToShouldBeBeforeRegisterByCourse
    $ NTop afRegisterTo <= NTop afRegisterByCourse

  warnValidation MsgAllocationFormRegisterByStaffFromShouldBeBeforeRegisterByCourse
    $ NTop afRegisterByStaffFrom <= NTop afRegisterByCourse

suggestAllocationForm :: OptionList TermId -> OptionList SchoolId
                      -> DB (Maybe AllocationForm)
suggestAllocationForm (Set.fromList . map optionInternalValue . olOptions -> terms) (Set.fromList . map optionInternalValue . olOptions -> schools) = runMaybeT $ do
  allocs <- lift . E.select . E.from $ \alloc -> do
    E.where_ $ alloc E.^. AllocationSchool `E.in_` E.valList (Set.toList schools)
    return ( alloc E.^. AllocationTerm
           , alloc E.^. AllocationSchool
           , alloc E.^. AllocationShorthand
           )
  let allocTerms = Map.fromListWith (<>) $ do
        (E.Value tid, E.Value ssh, E.Value ash) <- allocs
        return ((ssh, ash), Set.singleton tid)
      nextAlloc = flip Map.mapMaybe allocTerms $ \(Set.toDescList -> tids) -> case tids of
        TermKey t1 : TermKey t2 : _ -> Just . TermKey . toEnum $ fromEnum t1 + (fromEnum t1 - fromEnum t2)
        TermKey t1 : _              -> Just . TermKey $ succ t1
        _other                      -> Nothing
  maxAllocTerm <- hoistMaybe . fmap maximum . fromNullable $ Map.mapMaybe Set.lookupMax allocTerms
  ((ssh, ash), tid) <- hoistMaybe . fmap (minimumBy $ comparing (view _2) <> comparing (view _1)) . fromNullable . Map.toList $ Map.filter (\t -> t >= maxAllocTerm && t `Set.member` terms) nextAlloc
  oldTid <- hoistMaybe $ Set.lookupMax =<< Map.lookup (ssh, ash) allocTerms
  oldTerm <- MaybeT $ get oldTid
  newTerm <- MaybeT $ get tid
  Entity _ Allocation{..} <- MaybeT . getBy $ TermSchoolAllocationShort oldTid ssh ash

  let dayOffset = (diffDays `on` termLectureStart) newTerm oldTerm
      addTime = addLocalDays dayOffset

  return AllocationForm
    { afTerm = tid
    , afSchool = ssh
    , afShorthand = ash
    , afName = allocationName
    , afLegacyShorthands = Set.delete ash $ Set.fromList allocationLegacyShorthands
    , afDescription = allocationDescription
    , afStaffDescription = allocationStaffDescription
    , afStaffRegisterFrom = addTime <$> allocationStaffRegisterFrom
    , afStaffRegisterTo = addTime <$> allocationStaffRegisterTo
    , afStaffAllocationFrom = addTime <$> allocationStaffAllocationFrom
    , afStaffAllocationTo = addTime <$> allocationStaffAllocationTo
    , afRegisterFrom = addTime <$> allocationRegisterFrom
    , afRegisterTo = addTime <$> allocationRegisterTo
    , afRegisterByStaffFrom = addTime <$> allocationRegisterByStaffFrom
    , afRegisterByStaffTo = addTime <$> allocationRegisterByStaffTo
    , afRegisterByCourse = addTime <$> allocationRegisterByCourse
    , afOverrideDeregister = addTime <$> allocationOverrideDeregister
    }
