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

module Handler.Utils.Rating
  ( Rating(..), Rating'(..)
  , validateRating
  , getRating
  , formatRating
  , ratingFile
  , RatingException(..)
  , UnicodeException(..)
  , isRatingFile
  , parseRating
  , SubmissionContent
  , extractRatings
  ) where

import Import

import Text.PrettyPrint.Leijen.Text hiding ((<$>))

import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Text.Encoding.Error (UnicodeException(..))

import qualified Data.Text.Lazy.Encoding as Lazy.Text

import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI

import qualified Data.ByteString.Lazy as Lazy (ByteString)
import qualified Data.ByteString.Lazy as Lazy.ByteString

import Text.Read (readEither)

import System.FilePath
import qualified System.FilePath.Cryptographic as FilePath (decrypt)

import qualified Database.Esqueleto as E

import qualified Data.Conduit.List as Conduit

import Utils.Lens


instance HasResolution prec => Pretty (Fixed prec) where
  pretty = pretty . show

instance Pretty x => Pretty (CI x) where
  pretty = pretty . CI.original


instance Pretty SheetGrading where
  pretty Points{..}     = pretty ( show maxPoints <> " Punkte" :: String)
  pretty PassPoints{..} = pretty ( show maxPoints <> " Punkte, bestanden ab " <> show passingPoints <> " Punkte" :: String )
  pretty PassBinary     = pretty ( "Bestanden (1) /  Nicht bestanden (0)" :: String )


validateRating :: SheetType -> Rating' -> [RatingException]
validateRating ratingSheetType Rating'{ratingPoints=Just rp, ..}
  | rp < 0
    = [RatingNegative]
  | NotGraded <- ratingSheetType
    = [RatingNotExpected]
  | (Just maxPoints ) <- ratingSheetType ^?  _grading . _maxPoints
  , rp > maxPoints
    = [RatingExceedsMax]
  | (Just PassBinary) <- ratingSheetType ^? _grading
  , not (rp == 0 || rp == 1)
    = [RatingBinaryExpected]
validateRating _ _ = []

getRating :: SubmissionId -> YesodDB UniWorX (Maybe Rating)
getRating submissionId = runMaybeT $ do
  let query = E.select . E.from $ \(corrector `E.RightOuterJoin` (submission `E.InnerJoin` sheet `E.InnerJoin` course)) -> do
        E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
        E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet
        E.on $ corrector E.?. UserId E.==. submission E.^. SubmissionRatingBy

        E.where_ $ submission E.^. SubmissionId E.==. E.val submissionId

        -- Yes, we can only pass a tuple through 'E.select'
        return ( course E.^. CourseName
               , sheet E.^. SheetName
               , corrector E.?. UserDisplayName
               , sheet E.^. SheetType
               , submission E.^. SubmissionRatingPoints
               , submission E.^. SubmissionRatingComment
               , submission E.^. SubmissionRatingTime
               )

  [ ( E.unValue -> ratingCourseName
    , E.unValue -> ratingSheetName
    , E.unValue -> ratingCorrectorName
    , E.unValue -> ratingSheetType
    , E.unValue -> ratingPoints
    , E.unValue -> ratingComment
    , E.unValue -> ratingTime
    ) ] <- lift query

  return Rating{ ratingValues = Rating'{..}, .. }

formatRating :: CryptoFileNameSubmission -> Rating -> Lazy.ByteString
formatRating cID Rating{ ratingValues = Rating'{..}, ..} = let
  doc = renderPretty 1 45 $ foldr (<$$>) mempty
    [ "= Bitte nur Bewertung und Kommentare ändern ="
    , "============================================="
    , "========== Uni2work Bewertungsdatei ========="
    , "======= diese Datei ist UTF8 encodiert ======"
    , "Informationen zum Übungsblatt:"
    , indent 2 . foldr (<$$>) mempty . catMaybes $
      [ Just $ "Veranstaltung:" <+> pretty ratingCourseName
      , Just $ "Blatt:" <+> pretty ratingSheetName
      , ("Korrektor:" <+>) . pretty <$> ratingCorrectorName
      , ("Bewertung:" <+>) . pretty <$> (ratingSheetType ^? _grading)
      ]
    , "Abgabe-Id:" <+> pretty (Text.unpack $ toPathPiece cID)
    , "============================================="
    , "Bewertung:" <+> pretty ratingPoints
    , "=========== Beginn der Kommentare ==========="
    , pretty ratingComment
    ]
  in Lazy.Text.encodeUtf8 . (<> "\n") $ displayT doc

ratingFile :: MonadIO m => CryptoFileNameSubmission -> Rating -> m File
ratingFile cID rating@Rating{ ratingValues = Rating'{..}, .. } = do
  fileModified <- maybe (liftIO getCurrentTime) return ratingTime
  let
    fileTitle = "bewertung_" <> Text.unpack (toPathPiece cID) <.> "txt"
    fileContent = Just . Lazy.ByteString.toStrict $ formatRating cID rating
  return File{..}

parseRating :: MonadThrow m => File -> m Rating'
parseRating File{ fileContent = Just input, .. } = do
  inputText <- either (throw . RatingNotUnicode) return $ Text.decodeUtf8' input
  let
    (headerLines', commentLines) = break (commentSep `Text.isInfixOf`) $ Text.lines inputText
    (reverse -> ratingLines, reverse -> _headerLines) = break (sep' `Text.isInfixOf`) $ reverse headerLines'
    ratingLines' = filter (rating `Text.isInfixOf`) ratingLines
    commentSep = "Beginn der Kommentare"
    sep' = Text.pack $ replicate 40 '='
    rating = "Bewertung:"
  comment' <- case commentLines of
    (_:commentLines') -> return . Text.strip $ Text.unlines commentLines'
    _ -> throw RatingMissingSeparator
  let
    ratingComment
      | Text.null comment' = Nothing
      | otherwise = Just comment'
  ratingLine' <- case ratingLines' of
    [l] -> return l
    _ -> throw RatingMultiple
  let
    (_, ratingLine) = Text.breakOnEnd rating ratingLine'
    ratingStr = Text.unpack $ Text.strip ratingLine
  ratingPoints <- case () of
    _ | null ratingStr -> return Nothing
      | otherwise -> either (throw . RatingInvalid) return $ Just <$> readEither ratingStr
  return Rating'{ ratingTime = Just fileModified, .. }
parseRating _ = throwM RatingFileIsDirectory


type SubmissionContent = Either File (SubmissionId, Rating')

extractRatings :: ( MonadHandler m
                  , HandlerSite m ~ UniWorX
                  , MonadCatch m
                  ) => Conduit File m SubmissionContent
extractRatings = Conduit.mapM $ \f@File{..} -> do
  msId <- isRatingFile fileTitle
  case () of
    _ | Just sId <- msId
      , isJust fileContent
        -> Right . (sId, ) <$> parseRating f
      | otherwise -> return $ Left f

isRatingFile :: ( MonadHandler m
                , HandlerSite m ~ UniWorX
                , MonadCatch m
                ) => FilePath -> m (Maybe SubmissionId)
isRatingFile fName
  | Just cID <- isRatingFile' fName = do
      cIDKey <- getsYesod appCryptoIDKey
      (Just <$> FilePath.decrypt cIDKey cID) `catch` decryptErrors
  | otherwise = return Nothing
  where

    decryptErrors (CiphertextConversionFailed _) = return Nothing
    decryptErrors InvalidNamespaceDetected       = return Nothing
    decryptErrors DeserializationError           = return Nothing
    decryptErrors err                            = throwM err

isRatingFile' :: FilePath -> Maybe CryptoFileNameSubmission
isRatingFile' (takeFileName -> fName)
  | (bName, ".txt") <- splitExtension fName
  , Just piece <- stripPrefix "bewertung_" bName
  , Just cID <- fromPathPiece $ Text.pack piece
  = Just cID
  | otherwise
  = Nothing
