{-# LANGUAGE Trustworthy #-}
{-# LANGUAGE FlexibleContexts,
             MultiParamTypeClasses,
             FunctionalDependencies,
             FlexibleInstances,
             ScopedTypeVariables,
             TypeSynonymInstances #-}

{- |

This module exports classes 'DCRecord' and 'DCLabeledRecord' that
provide a way for Hails applications to interact with persistent data
more easily. Specifically, it provides a way to work with Haskell
types as opposed to \"unstructured\" 'Document's.

-}
module Hails.Database.Structured ( DCRecord(..)
                                 , findAll, findAllP
                                 , DCLabeledRecord(..)
                                 , toLabeledDocument, fromLabeledDocument
                                 , toLabeledDocumentP, fromLabeledDocumentP
                                 ) where

import           Data.Monoid (mappend, mempty)
import           Control.Monad (liftM)

import           LIO
import           LIO.DCLabel
                 
import           Hails.Data.Hson
import           Hails.PolicyModule
import           Hails.Database.Core
import           Hails.Database.Query
import           Hails.Database.TCB

-- | Class for converting from \"structured\" records to documents
-- (and vice versa). Minimal definition consists of 'toDocument',
-- 'fromDocument', and 'recordCollection'. All database operations
-- performed on the collection defined by 'recordCollection'.
class DCRecord a where
  -- | Convert a document to a record
  fromDocument :: Monad m => Document -> m a
  -- | Convert a record to a document
  toDocument :: a -> Document
  -- | Get the collection name for the record
  recordCollection :: a -> CollectionName
  -- | Find an object with matching value for the given key. If the
  -- object does not exist or cannot be read (its label is above the
  -- clearance), this returns 'Nothing'.
  findBy :: (BsonVal v, MonadDB m)
         => CollectionName -> FieldName -> v -> m (Maybe a)
  -- | Find an object with given query
  findWhere :: MonadDB m => Query -> m (Maybe a)
  -- | Insert a record into the database
  insertRecord :: MonadDB m => a -> m ObjectId
  -- | Update a record in the database
  saveRecord :: MonadDB m => a -> m ()
  -- | Same as 'findBy', but uses privileges. 
  findByP :: (BsonVal v, MonadDB m)
          => DCPriv -> CollectionName -> FieldName -> v -> m (Maybe a)
  -- | Same as 'findWhere', but uses privileges. 
  findWhereP :: MonadDB m => DCPriv -> Query -> m (Maybe a)
  -- | Same as 'insertRecord', but uses privileges. 
  insertRecordP :: MonadDB m => DCPriv -> a -> m ObjectId
  -- | Same as 'saveRecord', but uses privileges. 
  saveRecordP :: MonadDB m => DCPriv -> a -> m ()

  --
  -- Default definitions
  --

  --
  findBy = findByP mempty
  --
  findWhere = findWhereP mempty
  --
  insertRecord = insertRecordP mempty
  --
  saveRecord = saveRecordP mempty
  --
  insertRecordP p r = liftDB $ do
    insertP p (recordCollection r) $ toDocument r
  --
  saveRecordP p r = liftDB $ do
    saveP p (recordCollection r) $ toDocument r
  --
  findByP p cName k v = 
    findWhereP p (select [k -: v] cName)
  --
  findWhereP p query  = liftDB $ do
    mldoc <- findOneP p query
    c <- liftLIO $ getClearance
    case mldoc of
      Just ldoc | canFlowToP p (labelOf ldoc) c ->
                    fromDocument `liftM` (liftLIO $ unlabelP p ldoc)
      _ -> return Nothing
--   --
--   deleteByP p policy colName k v = 
--     deleteWhereP p policy (select [k =: v] colName)
--   --
--   deleteWhereP p policy sel = do
--     -- Find with only supplied privileges
--     mdoc <- findWhereP p policy $ select (selector sel) (coll sel)
--     -- User underlying privileges as well:
--     p' <- getPrivileges
--     res <- withDB policy $ deleteOneP (p' `mappend` p) sel
--     case res of
--       Right _ -> return mdoc
--       _ -> return Nothing
--   --


-- | Find all records that satisfy the query and can be read, subject
-- to the current clearance.
findAll :: (DCRecord a, MonadDB m) => Query -> m [a]
findAll = findAllP mempty

-- | Same as 'findAll', but uses privileges.
findAllP :: (DCRecord a, MonadDB m)
         => DCPriv -> Query -> m [a]
findAllP p query = liftDB $ do
  cursor <- findP p query
  cursorToRecords cursor []
  where cursorToRecords cur docs = do
          mldoc <- nextP p cur
          case mldoc of
            Just ldoc -> do
              c <- liftLIO getClearance
              if canFlowTo (labelOf ldoc) c
                then do md <- fromDocument `liftM` (liftLIO $ unlabelP p ldoc)
                        cursorToRecords cur $ maybe docs (:docs) md
                 else cursorToRecords cur docs
            _ -> return $ reverse docs

-- | Class used by a policy module to translate a labeled record to a
-- labeled document. Since the insert and save functions use the
-- policy module\'s privileges, only the policy module should be
-- allowed to create an instance of this class. Thus, we leverage the 
-- fact that the value constructor for a 'PolicyModule' is not exposed
-- to untrusted code and require the policy module to create such a
-- value in 'endorseInstance'. The minimal implementation needs to
-- define 'endorseInstance'.
class (PolicyModule pm, DCRecord a) => DCLabeledRecord pm a | a -> pm where
  -- | Insert a labeled record into the database.
  insertLabeledRecord :: MonadDB m => DCLabeled a -> m ObjectId
  -- | Insert a labeled record into the database
  saveLabeledRecord :: MonadDB m => DCLabeled a -> m ()

  -- | Same as 'insertLabeledRecord', but using explicit privileges.
  insertLabeledRecordP :: MonadDB m => DCPriv -> DCLabeled a -> m ObjectId
  -- | Same as 'saveLabeledRecord', but using explicit privileges.
  saveLabeledRecordP :: MonadDB m => DCPriv -> DCLabeled a -> m ()

  -- | Endorse the implementation of this instance. Note that this is
  -- reduced to WHNF to catch invalid instances that use 'undefined'.
  --
  -- Example implementation:
  --
  -- > endorseInstance _ = MyPolicyModuleTCB {- May leave other values undefined -}
  endorseInstance :: DCLabeled a -> pm

  --
  -- Default definitions for insert/save
  --

  --
  insertLabeledRecord lrec = insertLabeledRecordP mempty lrec
  --
  saveLabeledRecord lrec = saveLabeledRecordP mempty lrec
  --
  insertLabeledRecordP p lrec = liftDB $ do
    let cName = recordCollection (forceType lrec)
    ldoc <- toLabeledDocumentP p lrec
    insertP p cName ldoc

  --
  saveLabeledRecordP p lrec = liftDB $ do
    let cName = recordCollection (forceType lrec)
    ldoc <- toLabeledDocumentP p lrec
    saveP p cName ldoc

-- | Convert labeled record to labeled document.
toLabeledDocument :: (MonadDB m, DCLabeledRecord pm a)
                  => DCLabeled a
                  -> m (DCLabeled Document)
toLabeledDocument = toLabeledDocumentP mempty

-- | Uses the policy modules\'s privileges to convert a labeled record
-- to a labeled document, if the policy module created an instance of
-- 'DCLabeledRecord'.
toLabeledDocumentP :: (MonadDB m, DCLabeledRecord pm a)
                   => DCPriv
                   -> DCLabeled a -- ^ Labeled record
                   -> m (DCLabeled Document)
toLabeledDocumentP p' lr = liftDB $ do
  pmPriv' <- dbActionPriv `liftM` getActionStateTCB
  liftLIO $ do
    -- Fail if not endorsed:
    pmPriv <- (evaluate . endorseInstance $ lr) >> return pmPriv'
                      `catch` (\(_ :: SomeException) -> return mempty)
    let p = p' `mappend` pmPriv
    scopeClearance $ do
      -- raise clearance:
      clr <- getClearance
      setClearanceP p $ clr `lub` (p %% True)
      --
      r <- unlabelP p lr
      lcur <- getLabel
      let lres = downgradeP p lcur `lub` (labelOf lr)
      labelP p lres $ toDocument r

-- | Convert labeled document to labeled record
fromLabeledDocument :: forall m pm a. (MonadDB m, DCLabeledRecord pm a)
                    => DCLabeled Document
                    -> m (DCLabeled a)
fromLabeledDocument = fromLabeledDocumentP mempty

-- | Uses the policy modules\'s privileges to convert a labeled document
-- to a labeled record, if the policy module created an instance of
-- 'DCLabeledRecord'.
fromLabeledDocumentP :: forall m pm a. (MonadDB m, DCLabeledRecord pm a)
                     => DCPriv
                     -> DCLabeled Document
                     -> m (DCLabeled a)
fromLabeledDocumentP p' ldoc = liftDB $ do
  pmPriv' <- dbActionPriv `liftM` getActionStateTCB
  -- Fail if not endorsed:
  pmPriv <- liftLIO $ (evaluate . endorseInstance $ fake) >> return pmPriv'
                      `catch` (\(_ :: SomeException) -> return mempty)
  let p = p' `mappend` pmPriv
  liftLIO $ scopeClearance $ do
    -- raise clearance:
    clr <- getClearance
    setClearanceP p $ clr `lub` (p %% True)
    -- get at the document
    doc <- liftLIO $ unlabelP p ldoc
    lcur <- liftLIO $ getLabel
    let lres = downgradeP p lcur `lub` (labelOf ldoc)
    rec <- fromDocument doc
    labelP p lres rec
    where fake :: DCLabeled a
          fake = undefined

--
-- Misc helpers
--

-- | Get the type of a 'DCLabeled' value
forceType :: DCLabeled a -> a
forceType = undefined