module Hackage.Security.Client.Verify (
    -- * Verification monad
    Verify -- opaque
  , runVerify
  , acquire
  , ifVerified
    -- * Specific resources
  , openTempFile
    -- * Re-exports
  , liftIO
  ) where

import Prelude
import Control.Exception
import Control.Monad (join, void)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Reader (ReaderT, runReaderT, ask)
import Data.IORef

import Hackage.Security.Util.IO
import Hackage.Security.Util.Path

{-------------------------------------------------------------------------------
  Verification monad
-------------------------------------------------------------------------------}

type Finaliser = IO ()
type Cleanup   = IO ()

-- | Verification monad
--
-- The verification monad is similar to 'ResourceT' in intent, in that we can
-- register handlers to be run to release resources. Unlike 'ResourceT',
-- however, we maintain _two_ handlers: a cleanup handler which is run  whether
-- or not verification succeeds, and a finalisation handler which is run only if
-- verification succeeds.
--
-- * Cleanup handlers are registered using 'acquire', and are guaranteed to run
--   just before the computation terminates (after the finalisation handler).
-- * The finalisation handlers are run only when verification succeeds, and can
--   be registered with 'ifVerified'. Finalisation can be used for instance to
--   update the local cache (which should only happen if verification is
--   successful).
newtype Verify a = Verify {
    unVerify :: ReaderT (IORef Cleanup, IORef Finaliser) IO a
  }
  deriving (Functor, Applicative, Monad, MonadIO)

-- | Run an action in the 'Verify' monad
runVerify :: (Finaliser -> Finaliser) -> Verify a -> IO a
runVerify modifyFinaliser v = do
    rCleanup   <- newIORef $ return ()
    rFinaliser <- newIORef $ return ()
    mask $ \restore -> do
      ma <- try $ restore $ runReaderT (unVerify v) (rCleanup, rFinaliser)
      case ma of
        Left ex -> do join $ readIORef rCleanup
                      throwIO (ex :: SomeException)
        Right a -> do modifyFinaliser $ join $ readIORef rFinaliser
                      join $ readIORef rCleanup
                      return a

-- | Acquire a resource and register the corresponding cleanup handler
--
-- NOTE: Resource acquisition happens with exceptions masked. If it is important
-- that the resource acquistion can be timed out (or receive other kinds of
-- asynchronous exceptions), you will need to use an interruptible operation.
-- See <http://www.well-typed.com/blog/2014/08/asynchronous-exceptions/> for
-- details.
acquire :: IO a -> (a -> IO ()) -> Verify a
acquire get release = Verify $ do
    (rCleanup, _rFinaliser) <- ask
    liftIO $ mask_ $ do
      a <- liftIO get
      modifyIORef rCleanup (>> release a)
      return a

-- | Register an action to be run only if verification succeeds
ifVerified :: IO () -> Verify ()
ifVerified handler = Verify $ do
    (_rCleanup, rFinaliser) <- ask
    liftIO $ modifyIORef rFinaliser (>> handler)

{-------------------------------------------------------------------------------
  Specific resources
-------------------------------------------------------------------------------}

-- | Create a short-lived temporary file
--
-- Creates the directory where the temp file should live if it does not exist.
openTempFile :: FsRoot root
             => Path root  -- ^ Temp directory
             -> String     -- ^ Template
             -> Verify (Path Absolute, Handle)
openTempFile tmpDir template =
    acquire createTempFile closeAndDelete
  where
    createTempFile :: IO (Path Absolute, Handle)
    createTempFile = do
      createDirectoryIfMissing True tmpDir
      openTempFile' tmpDir template

    closeAndDelete :: (Path Absolute, Handle) -> IO ()
    closeAndDelete (fp, h) = do
      hClose h
      void $ handleDoesNotExist $ removeFile fp
