{-|
Module      : Handler.Utils.Delete
Description : Generic deletion from database after confirmation

`postDeleteR`, `getDeleteR`, and `deleteR` provide handlers for calling
`deleteCascade` on a `Set` of Record-`Key`s after asking for confirmation, which
currently entails asking the user to copy a text, which is dependent on the
records to be deleted (i.e. a comma-separated list of user names), into a
`Textarea`.
-}
module Handler.Utils.Delete
  ( DeleteRoute(..)
  , deleteR
  , postDeleteR, getDeleteR
  , JobDB
  ) where

import Import
import Handler.Utils.Form

import qualified Data.Text as Text
import qualified Data.Set as Set

import qualified Data.CaseInsensitive as CI

import Data.Char (isAlphaNum)

import qualified Database.Esqueleto.Legacy as E
import qualified Database.Esqueleto.Internal.Internal as E (SqlSelect)

import Jobs.Queue

{-# ANN deleteR ("HLint: ignore Use const" :: String) #-}


data DeleteRoute record = forall tables infoExpr info. (E.SqlSelect infoExpr info, E.From tables) => DeleteRoute
  { drRecords :: Set (Key record) -- ^ Records to be deleted
  , drGetInfo :: tables -> E.SqlQuery infoExpr -- ^ SQL-Query to get necessary information to render identifing information about records to the user (`drRenderRecord`, `drRecordConfirmString`); @tables@ is an arbitrary join, see `E.from`; @infoExpr@ gets converted to @info@ by esqueleto
  , drUnjoin :: tables -> E.SqlExpr (Entity record) -- ^ `E.SqlExpr` of @Key record@ extracted from @tables@, `deleteR` restricts `drGetInfo` to `drRecords` automatically
  , drRenderRecord :: info -> DB Widget -- ^ Present a single record, to be deleted, to the user for inspection prior to deletion
  , drRecordConfirmString :: info -> DB Text -- ^ Text for the user to copy to confirm deletion; should probably contain all information from `drRenderRecord` so user gets prompted to think about what they're deleting
  , drFormMessage :: [info] -> DB (Maybe Message)
  , drCaption
  , drSuccessMessage :: SomeMessage UniWorX
  , drAbort
  , drSuccess :: SomeRoute UniWorX
  , drDelete :: forall a. Key record -> JobDB a -> JobDB a
  }

confirmForm :: ( MonadHandler m, HandlerSite m ~ UniWorX )
           => Text -- ^ Confirmation string
           -> AForm m Bool
confirmForm confirmString = flip traverseAForm aform $ \(inpConfirmStr, BtnDelete) -> if
  | ((==) `on` map (CI.mk . filter isAlphaNum) . Text.words) confirmString inpConfirmStr
    -> return $ pure True
  | otherwise
    -> formFailure [MsgDeleteConfirmationWrong]
  where
    aform = (,)
        <$> areq confirmField (fslI MsgDeleteConfirmation) Nothing
        <*> pure BtnDelete
    confirmField
      | multiple  = convertField unTextarea Textarea textareaField
      | otherwise = textField
    multiple = length (filter (not . Text.null . Text.strip) $ Text.lines confirmString) > 1

confirmFormReduced :: Monad m => AForm m Bool
confirmFormReduced = pure True

confirmForm' :: PersistEntity record => Set (Key record) -> Text -> Maybe Message -> Form Bool
confirmForm' drRecords confirmString mmsg = identifyForm FIDDelete . addDeleteTargets . renderAForm FormStandard . maybe id ((*>) . aformMessage) mmsg . maybe confirmFormReduced confirmForm $ assertM' (not . Text.null . Text.strip) confirmString
  where
    addDeleteTargets :: Form a -> Form a
    addDeleteTargets form csrf = do
      (_, fvTargets) <- mreq secretJsonField ("" & addName (toPathPiece PostDeleteTarget)) (Just drRecords)
      over _2 (mappend $ fvInput fvTargets) <$> form csrf


postDeleteR :: (PersistEntity record, PersistEntityBackend record ~ SqlBackend)
            => (Set (Key record) -> DeleteRoute record) -- ^ Construct `DeleteRoute` based on incoming record keys
            -> Handler ()
-- | Perform deletion
postDeleteR mkRoute = do
  drResult <- fmap (fmap mkRoute) . runInputPost . iopt secretJsonField $ toPathPiece PostDeleteTarget

  traverse_ deleteR' drResult

getDeleteR :: (PersistEntity record, PersistEntityBackend record ~ SqlBackend)
           => DeleteRoute record -> Handler a
getDeleteR = deleteR'

deleteR' :: (PersistEntity record, PersistEntityBackend record ~ SqlBackend)
         => DeleteRoute record -> Handler a
deleteR' DeleteRoute{..} = do
  (targets, confirmString, message) <- runDB $ do
    infos <- E.select . E.from $ \t -> do
      E.where_ $ drUnjoin t E.^. persistIdField `E.in_` E.valList (Set.toList drRecords)
      drGetInfo t

    targets <- mapM (runKleisli $ Kleisli drRenderRecord &&& Kleisli drRecordConfirmString) infos
    let confirmString = Text.unlines $ view _2 <$> targets
    message <- drFormMessage infos

    return (targets, confirmString, message)

  ((confirmRes, deleteFormWdgt), deleteFormEnctype) <- runFormPost $ confirmForm' drRecords confirmString message

  formResult confirmRes $ \case
    True -> do
      runDBJobs $ do
        forM_ drRecords $ \k -> drDelete k $ delete k
        addMessageI Success drSuccessMessage
      redirect drSuccess
    False ->
      redirect drAbort

  targetRoute <- fromMaybe (error "getDeleteR called from 404-handler") <$> getCurrentRoute
  let deleteForm = wrapForm' BtnDelete deleteFormWdgt def
        { formAction = Just $ SomeRoute targetRoute
        , formEncoding = deleteFormEnctype
        , formSubmit = FormSubmit
        }

  sendResponse =<<
    defaultLayout $(widgetFile "widgets/delete-confirmation/delete-confirmation")



deleteR :: (PersistEntity record, PersistEntityBackend record ~ SqlBackend)
        => DeleteRoute record -> Handler Html
deleteR dr = do
  postDeleteR $ \drRecords -> dr {drRecords}
  getDeleteR dr
