{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
-- | This module implements commands to communicate with the LXD daemon
-- over its REST API.
--
-- More information about LXD: <https://github.com/lxc/lxd>
--
-- This module implements a high-level interface, and is probably what
-- you need. It uses the lower-level interface implemented in
-- "Network.LXD.Client.API", but unless you are a power user, you
-- shouldn't need this module.
--
-- Accompanying blog post:
-- <https://deliquus.com/posts/2017-10-02-using-servant-to-orchestrate-lxd-containers.md>
--
module Network.LXD.Client.Commands (
  -- * How to use this library
  -- $use

  -- * Re-exports
  def
, module Network.LXD.Client.Remotes
, module Network.LXD.Client.Types

  -- * Running commands
, Host(..)
, HasClient(..)
, defaultClientEnv
, WithLocalHost, runWithLocalHost
, WithRemoteHost, runWithRemoteHost

  -- * API
, lxcApi
  -- * Containers
, lxcList
, lxcCreate
, lxcDelete
, lxcInfo
, lxcStart
, lxcStop
, lxcRestart
, lxcFreeze
, lxcUnfreeze

  -- * Exec
, lxcExec
, lxcExecEnv
, lxcExecRaw

  -- * Files and directories
  -- ** Deletion
, lxcFileDelete
  -- ** Files
, lxcFilePull
, lxcFilePullRaw
, lxcFilePush
, lxcFilePushAttrs
, lxcFilePushRaw
, lxcFilePushRawAttrs
  -- ** Directories
, lxcFileListDir
, lxcFileMkdir
, lxcFileMkdirTemplate
, lxcFileMkdirAttrs
  -- ** Recursive
, lxcFilePullRecursive
, lxcFilePushRecursive
, lxcFilePushRecursiveAttrs

  -- * Images
, lxcImageList
, lxcImageAliases
, lxcImageInfo
, lxcImageAlias
, lxcImageCreate
, lxcImageDelete

  -- * Networks
, lxcNetworkList
, lxcNetworkCreate
, lxcNetworkInfo
, lxcNetworkConfig
, lxcNetworkDelete

  -- * Profiles
, lxcProfileList
, lxcProfileCreate
, lxcProfileInfo
, lxcProfileConfig
, lxcProfileDelete

  -- * Storage
, lxcStorageList
, lxcStorageCreate
, lxcStorageInfo
, lxcStorageConfig
, lxcStorageDelete

  -- * Volume
, lxcVolumeList
, lxcVolumeCreate
, lxcVolumeInfo
, lxcVolumeConfig
, lxcVolumeDelete
) where

import Network.LXD.Client.Internal.Prelude

import Control.Concurrent.Async (Async, async, withAsync, wait, uninterruptibleCancel)
import Control.Concurrent.MVar
import Control.Monad ((>=>))
import Control.Monad.Catch (Exception, SomeException, MonadThrow, throwM, MonadCatch, catch, MonadMask, bracket)
import Control.Monad.State (StateT, evalStateT, gets, modify')

import Data.ByteString.Lazy (ByteString)
import Data.Default (def)
import Data.List (inits)
import Data.Map.Strict (Map)
import qualified Data.Aeson.Types as Aeson
import qualified Data.ByteString.Lazy as BL

import Numeric (readOct, showOct)
import qualified Network.WebSockets as WS

import Servant.Client (ClientM, ClientEnv, runClientM)
import System.Directory (createDirectory, doesDirectoryExist, listDirectory)
import System.FilePath ((</>), splitPath)
import System.Posix.Files (getFileStatus, fileMode, setFileMode)
import qualified System.IO as IO

import Network.LXD.Client hiding (Host)
import Network.LXD.Client.API
import Network.LXD.Client.Types
import Network.LXD.Client.Events
import Network.LXD.Client.Remotes

-- | A host that can be connected to.
data Host = HLocalHost LocalHost
          | HRemoteHost RemoteHost

-- | Monad with access to a 'ClientEnv'.
class (MonadIO m, MonadMask m) => HasClient m where
    -- | Return the LXD remote host to connect to.
    askHost :: m Host

    -- | Return the 'ClientEnv' to use when connecting to the LXD host.
    --
    -- Returns 'defaultClientEnv' by default.
    askClientEnv :: m ClientEnv
    askClientEnv = defaultClientEnv

-- | Create a default 'ClientEnv'.
defaultClientEnv :: HasClient m => m ClientEnv
defaultClientEnv = do
    host <- askHost
    liftIO (runExceptT $ client host) >>= \case
        Left err -> throwM $ ClientError err
        Right env -> return env
 where
   client (HLocalHost host) = localHostClient host
   client (HRemoteHost host) = remoteHostClient host

-- | Run a web sockets application using.
runWebSockets :: Host -> String -> WS.ClientApp a -> IO a
runWebSockets host' path app =
    runExceptT runWS >>= \case
        Left err -> throwM $ ClientError err
        Right env -> return env
  where
    runWS | HLocalHost host <- host' = runWebSocketsLocal host path app
          | HRemoteHost host <- host' = runWebSocketsRemote host path app

-- | Monad with access to a local host.
newtype WithLocalHost a = WithLocalHost (StateT (LocalHost, Maybe ClientEnv) IO a)
                        deriving (Applicative, Functor, Monad, MonadIO, MonadCatch, MonadThrow, MonadMask)

instance HasClient WithLocalHost where
    askHost = WithLocalHost . gets $ HLocalHost . fst
    askClientEnv = do
        env <- WithLocalHost . gets $ snd
        case env of
            Just env' -> return env'
            Nothing -> do
                env' <- defaultClientEnv
                WithLocalHost . modify' $ \(x, _) -> (x, Just env')
                return env'

-- | Run a 'WithLocalHost' monad
runWithLocalHost :: LocalHost -> WithLocalHost a -> IO a
runWithLocalHost host (WithLocalHost m) = evalStateT m (host, Nothing)

-- | Monad with access to a remote host.
newtype WithRemoteHost a = WithRemoteHost (StateT (RemoteHost, Maybe ClientEnv) IO a)
                        deriving (Applicative, Functor, Monad, MonadIO, MonadCatch, MonadThrow, MonadMask)

instance HasClient WithRemoteHost where
    askHost = WithRemoteHost . gets $ HRemoteHost . fst
    askClientEnv = do
        env <- WithRemoteHost . gets $ snd
        case env of
            Just env' -> return env'
            Nothing -> do
                env' <- defaultClientEnv
                WithRemoteHost . modify' $ \(x, _) -> (x, Just env')
                return env'

-- | Run a 'WithRemoteHost' monad
runWithRemoteHost :: RemoteHost -> WithRemoteHost a -> IO a
runWithRemoteHost host (WithRemoteHost m) = evalStateT m (host, Nothing)

-- | Get information about the API.
lxcApi :: HasClient m => m ApiConfig
lxcApi = runClient $ apiConfig >>= checkResponseOK

-- | List all container names.
lxcList :: HasClient m => m [ContainerName]
lxcList = runClient $ containerNames >>= checkResponseOK

-- | Create a new container.
lxcCreate :: HasClient m => ContainerCreateRequest -> m ()
lxcCreate req = runAndWait $ containerCreate req >>= checkResponseCreated

-- | Delete a container.
lxcDelete :: HasClient m => ContainerName -> m ()
lxcDelete n = runAndWait $ containerDelete n ContainerDeleteRequest
                           >>= checkResponseCreated

-- | Get information about a container.
lxcInfo :: HasClient m => ContainerName -> m Container
lxcInfo = runClient . (container >=> checkResponseOK)

-- | Start a contianer.
lxcStart :: HasClient m => ContainerName -> m ()
lxcStart name = runAndWait $ containerPutState name s >>= checkResponseCreated
  where s = containerNewState Start False

-- | Stop a container.
--
-- The second flag forces the action.
lxcStop :: HasClient m => ContainerName -> Bool -> m ()
lxcStop name force = runAndWait $ containerPutState name s >>= checkResponseCreated
  where s = containerNewState Stop force

-- | Restart a container.
--
-- The second flag forces the action.
lxcRestart :: HasClient m => ContainerName -> Bool -> m ()
lxcRestart name force = runAndWait $ containerPutState name s >>= checkResponseCreated
  where s = containerNewState Restart force

-- | Freeze a container.
lxcFreeze :: HasClient m => ContainerName -> m ()
lxcFreeze name = runAndWait $ containerPutState name s >>= checkResponseCreated
  where s = containerNewState Freeze False

-- | Unfreeze a container.
lxcUnfreeze :: HasClient m => ContainerName -> m ()
lxcUnfreeze name = runAndWait $ containerPutState name s >>= checkResponseCreated
  where s = containerNewState Unfreeze False

-- | Execute a command, catch standard output, print stderr.
lxcExec :: HasClient m
        => ContainerName            -- ^ Container name
        -> String                   -- ^ Command name
        -> [String]                 -- ^ Command arguments
        -> ByteString               -- ^ Standard input
        -> m ByteString
lxcExec name cmd args = lxcExecEnv name cmd args mempty

-- | Execute a command, provide environment variables, catch standard output,
-- print stderr.
lxcExecEnv :: HasClient m
           => ContainerName            -- ^ Container name
           -> String                   -- ^ Command name
           -> [String]                 -- ^ Command arguments
           -> Map String String        -- ^ Environment variables
           -> ByteString               -- ^ Standard input
           -> m ByteString
lxcExecEnv name cmd args env stdin' = do
    stdin  <- liftIO newEmptyMVar
    stdout <- liftIO newEmptyMVar
    stderr <- liftIO newEmptyMVar
    bs     <- liftIO $ newMVar mempty

    let printStderr :: IO ()
        printStderr = takeMVar stderr
                      >>= BL.hPut IO.stdout
                      >>  IO.hFlush IO.stdout
                      >>  printStderr

    let saveStdout :: IO ()
        saveStdout = do bs' <- takeMVar stdout
                        modifyMVar_ bs $ return . (<> bs')

    exec <- lxcExecRaw name cmd args env stdin stdout stderr
    liftIO $
        withAsync printStderr $ \_ ->
        withAsync saveStdout  $ \_ -> do
            unless (BL.null stdin') $ putMVar stdin (Just stdin')
            putMVar stdin Nothing
            wait exec

    liftIO $ takeMVar bs

-- | Execute a command, with given environment variables.
lxcExecRaw :: HasClient m
           => ContainerName            -- ^ Container name
           -> String                   -- ^ Command name
           -> [String]                 -- ^ Command arguments
           -> Map String String        -- ^ Environment variables
           -> MVar (Maybe ByteString)  -- ^ Stream of standard input, pass 'Nothing' to end the stream.
           -> MVar ByteString          -- ^ Standard output
           -> MVar ByteString          -- ^ Standard error
           -> m (Async ())
lxcExecRaw name cmd args env stdin stdout stderr = do
    resp <- runClient $ containerExecWebsocketNonInteractive name req
    md   <- checkResponseCreatedMetadata resp
    let oid     = responseOperation resp
        fds     = execResponseMetadataWebsocketFds md
        stdout' = operationWebSocket oid (fdsAllStdout fds)
        stderr' = operationWebSocket oid (fdsAllStderr fds)
        stdin'  = operationWebSocket oid (fdsAllStdin  fds)

    host <- askHost
    let runWS = runWebSockets host

    liftIO . async $
        withAsync (runWS stdout' $ readAllWebSocket (putMVar stdout)) $ \stdoutThread ->
        withAsync (runWS stderr' $ readAllWebSocket (putMVar stderr)) $ \stderrThread ->
        withAsync (runWS stdin'  $ writeAllWebSocket stdin)           $ \stdinThread -> do
            wait stdoutThread
            wait stderrThread
            wait stdinThread
  where
    req = def { execRequestCommand = cmd:args
              , execRequestEnvironment = env }

-- | Delete a file or empty directory from an LXD container.
lxcFileDelete :: HasClient m => ContainerName -> FilePath -> m ()
lxcFileDelete name fp = void . runClient $ containerDeletePath name fp

-- | Pull the file contents from an LXD container.
lxcFilePull :: HasClient m
            => ContainerName  -- ^ Container name
            -> FilePath       -- ^ Source path, in the container
            -> FilePath       -- ^ Destination path, in the host
            -> m ()
lxcFilePull name src dst = do
    path <- runClient $ containerGetPath name src
    case getFile path of
        Directory _ -> throwM $ ClientError "expected a file, but got a directory"
        File bs -> do
            m' <- convFileMode' $ pathMode path
            liftIO $ BL.writeFile dst bs
            liftIO $ setFileMode dst m'

    lxcFilePullRaw name src >>= liftIO . BL.writeFile dst

-- | Pull the file contents from an LXD container, return the lazy bytestring.
lxcFilePullRaw :: HasClient m => ContainerName -> FilePath -> m ByteString
lxcFilePullRaw name src = do
    path <- runClient $ containerGetPath name src
    case getFile path of
        File bs -> return bs
        Directory _ -> throwM $ ClientError "expected a file, but got a directory"

-- | Push the file contents to an LXD container.
lxcFilePush :: HasClient m
            => ContainerName  -- ^ Container name
            -> FilePath       -- ^ Source path, in the host
            -> FilePath       -- ^ Destination path, in the container
            -> m ()
lxcFilePush name src dst = lxcFilePushAttrs name src dst Nothing Nothing

-- | Push the fole contents to an LXD container, with the given attributes.
lxcFilePushAttrs :: HasClient m
                 => ContainerName  -- ^ Container name
                 -> FilePath       -- ^ Source path, in the host
                 -> FilePath       -- ^ Destination path, in the container
                 -> Maybe Uid
                 -> Maybe Gid
                 -> m ()
lxcFilePushAttrs name src dst uid gid = do
    mode <- liftIO $ fileMode <$> getFileStatus src
    bs   <- liftIO $ BL.readFile src
    lxcFilePushRawAttrs name dst
                        uid gid
                        (Just $ convFileMode mode)
                        "file"
                        Nothing
                        bs

-- | Write the lazy bytestring to a file in an LXD container.
lxcFilePushRaw :: HasClient m => ContainerName -> FilePath -> ByteString -> m ()
lxcFilePushRaw name src = lxcFilePushRawAttrs name src n n n "file" n where n = Nothing

-- | Write the lazy bytestring to a file in an LXD container, with given file
-- attributes.
lxcFilePushRawAttrs :: HasClient m
                    => ContainerName
                    -> FilePath
                    -> Maybe Uid
                    -> Maybe Gid
                    -> Maybe FileMode
                    -> FileType
                    -> Maybe WriteMode
                    -> ByteString
                    -> m ()
lxcFilePushRawAttrs name src uid gid fm ft wm bs = void . runClient $
    containerPostPath name src uid gid fm ft wm bs >>= checkResponseOK

-- | Create a directory using a host directory as a template.
--
-- Note that this function doesn't copy the directory contents. Use
-- 'lxcFilePushRecursive' if you want to copy the directory contents as well.
lxcFileMkdirTemplate :: HasClient m
                     => ContainerName  -- ^ Container name
                     -> FilePath       -- ^ Source path, in the host
                     -> FilePath       -- ^ Destination path, in the container
                     -> m ()
lxcFileMkdirTemplate name src dst = do
    mode <- liftIO $ fileMode <$> getFileStatus src
    lxcFileMkdirAttrs name dst False Nothing Nothing (Just $ convFileMode mode)

-- | List all entries in a directory, without @.@ or @..@.
lxcFileListDir :: HasClient m => ContainerName -> FilePath -> m [String]
lxcFileListDir name fp = do
    path <- runClient $ containerGetPath name fp
    case getFile path of
        File _ -> throwM $ ClientError "expected a directory, but got a file"
        Directory r -> checkResponseOK r

-- | Create a directory.
lxcFileMkdir :: HasClient m
             => ContainerName
             -> String
             -> Bool          -- ^ Create parent directories
             -> m ()
lxcFileMkdir name dir p = lxcFileMkdirAttrs name dir p Nothing Nothing Nothing

-- | Create a directory, with given attributes.
lxcFileMkdirAttrs :: HasClient m
                  => ContainerName
                  -> String
                  -> Bool           -- ^ Create parent directories
                  -> Maybe Uid
                  -> Maybe Gid
                  -> Maybe FileMode
                  -> m ()
lxcFileMkdirAttrs name dir False uid gid fm = void . runClient $
    containerPostPath name dir uid gid fm "directory" Nothing mempty
lxcFileMkdirAttrs name dir True uid gid fm =
    mapM_ mkdir $ inits (splitPath dir)
  where
    mkdir xs = case concat xs of
        "/" -> return ()
        path -> do
            pathInfo <- runClient $ (Just <$> containerGetPath name path) `catch` ignoreExc
            case getFile <$> pathInfo of
                Just (File _) -> throwM $ ClientError $ "couldn't make dir " ++ dir ++ ": " ++ path ++ " is not a directory"
                Just (Directory _) -> return ()
                Nothing -> lxcFileMkdirAttrs name dir False uid gid fm

    ignoreExc :: Monad m => SomeException -> m (Maybe a)
    ignoreExc _ = return Nothing

-- | Recursively pull a directory (or file) from a container.
lxcFilePullRecursive :: HasClient m
                     => ContainerName  -- ^ Container name
                     -> FilePath       -- ^ Source path, in the container
                     -> FilePath       -- ^ Destination path, in the host
                     -> m ()
lxcFilePullRecursive name src dst = do
    path <- runClient $ containerGetPath name src
    m'   <- convFileMode' $ pathMode path
    case getFile path of
        File bs -> do
            liftIO $ BL.writeFile dst bs
            liftIO $ setFileMode dst m'
        Directory resp -> do
            contents <- checkResponseOK resp
            liftIO $ createDirectory dst
            mapM_ go contents
            liftIO $ setFileMode dst m'
  where
    go file = lxcFilePullRecursive name src' dst'
      where src' = src </> file
            dst' = dst </> file

-- | Recursively push a directory (or file) to a container.
lxcFilePushRecursive :: HasClient m
                     => ContainerName  -- ^ Container name
                     -> FilePath       -- ^ Source path, in the host
                     -> FilePath       -- ^ Destination path, in the container
                     -> m ()
lxcFilePushRecursive name src dst = lxcFilePushRecursiveAttrs name src dst Nothing Nothing

-- | Recursively push a directory (or file) to a container, with given file
-- attributes.
lxcFilePushRecursiveAttrs :: HasClient m
                          => ContainerName
                          -> FilePath       -- ^ Souce path, in the host
                          -> FilePath       -- ^ Destination path, in the container
                          -> Maybe Uid
                          -> Maybe Gid
                          -> m ()
lxcFilePushRecursiveAttrs name src dst uid gid = do
    isDir <- liftIO $ doesDirectoryExist src
    if not isDir
        then lxcFilePushAttrs name src dst uid gid
        else do
            lxcFileMkdirTemplate name src dst
            files <- liftIO $ listDirectory src
            mapM_ go files
  where
    go file = lxcFilePushRecursiveAttrs name src' dst' uid gid
      where src' = src </> file
            dst' = dst </> file

-- | List all image IDs.
lxcImageList :: HasClient m => m [ImageId]
lxcImageList = runClient $ imageIds >>= checkResponseOK

-- | List al image aliases.
lxcImageAliases :: HasClient m => m [ImageAliasName]
lxcImageAliases = runClient $ imageAliases >>= checkResponseOK

-- | Get image information.
lxcImageInfo :: HasClient m => ImageId -> m Image
lxcImageInfo = runClient . image >=> checkResponseOK

-- | Get image alias information.
lxcImageAlias :: HasClient m => ImageAliasName -> m ImageAlias
lxcImageAlias = runClient . imageAlias >=> checkResponseOK

-- | Create an image.
lxcImageCreate :: HasClient m => ImageCreateRequest -> m ()
lxcImageCreate req = runAndWait $ imageCreate req >>= checkResponseCreated

-- | Delete an image.
lxcImageDelete :: HasClient m => ImageId -> m ()
lxcImageDelete img = runAndWait $ imageDelete img def >>= checkResponseCreated

-- | List all networks
lxcNetworkList :: HasClient m => m [NetworkName]
lxcNetworkList = runClient $ networkList >>= checkResponseOK

-- | Create a network.
lxcNetworkCreate :: HasClient m => NetworkCreateRequest -> m ()
lxcNetworkCreate n = void . runClient $ networkCreate n >>= checkResponseOK

-- | Get network information.
lxcNetworkInfo :: HasClient m => NetworkName -> m Network
lxcNetworkInfo n = runClient $ network n >>= checkResponseOK

-- | Configure a network.
lxcNetworkConfig :: HasClient m => NetworkName -> NetworkConfigRequest -> m ()
lxcNetworkConfig n c = void . runClient $ networkPatch n c >>= checkResponseOK

-- | Delete a network
lxcNetworkDelete :: HasClient m => NetworkName -> m ()
lxcNetworkDelete n = void . runClient $ networkDelete n >>= checkResponseOK

-- | List all profiles
lxcProfileList :: HasClient m => m [ProfileName]
lxcProfileList = runClient $ profileList >>= checkResponseOK

-- | Create a profile.
lxcProfileCreate :: HasClient m => ProfileCreateRequest -> m ()
lxcProfileCreate n = void . runClient $ profileCreate n >>= checkResponseOK

-- | Get profile information.
lxcProfileInfo :: HasClient m => ProfileName -> m Profile
lxcProfileInfo n = runClient $ profile n >>= checkResponseOK

-- | Configure a profile.
lxcProfileConfig :: HasClient m => ProfileName -> ProfileConfigRequest -> m ()
lxcProfileConfig n c = void . runClient $ profilePatch n c >>= checkResponseOK

-- | Delete a profile
lxcProfileDelete :: HasClient m => ProfileName -> m ()
lxcProfileDelete n = void . runClient $ profileDelete n >>= checkResponseOK

-- | List all storage pools
lxcStorageList :: HasClient m => m [PoolName]
lxcStorageList = runClient $ poolList >>= checkResponseOK

-- | Create a storage pool.
lxcStorageCreate :: HasClient m => PoolCreateRequest -> m ()
lxcStorageCreate n = void . runClient $ poolCreate n >>= checkResponseOK

-- | Get storage pool information.
lxcStorageInfo :: HasClient m => PoolName -> m Pool
lxcStorageInfo n = runClient $ pool n >>= checkResponseOK

-- | Configure a storage pool.
lxcStorageConfig :: HasClient m => PoolName -> PoolConfigRequest -> m ()
lxcStorageConfig n c = void . runClient $ poolPatch n c >>= checkResponseOK

-- | Delete a storage pool
lxcStorageDelete :: HasClient m => PoolName -> m ()
lxcStorageDelete n = void . runClient $ poolDelete n >>= checkResponseOK

-- | List all volumes
lxcVolumeList :: HasClient m => PoolName -> m [VolumeName]
lxcVolumeList p = runClient $ volumeList p >>= checkResponseOK

-- | Create a volume.
lxcVolumeCreate :: HasClient m => PoolName -> VolumeCreateRequest -> m ()
lxcVolumeCreate p r = void . runClient $ volumeCreate p r >>= checkResponseOK

-- | Get volume information.
lxcVolumeInfo :: HasClient m => PoolName -> VolumeName -> m Volume
lxcVolumeInfo p n = runClient $ volume p n >>= checkResponseOK

-- | Configure a volume.
lxcVolumeConfig :: HasClient m => PoolName -> VolumeName -> VolumeConfigRequest -> m ()
lxcVolumeConfig p n c = void . runClient $ volumePatch p n c >>= checkResponseOK

-- | Delete a volume
lxcVolumeDelete :: HasClient m => PoolName -> VolumeName -> m ()
lxcVolumeDelete p n = void . runClient $ volumeDelete p n >>= checkResponseOK

-- | Run a client operation.
runClient :: HasClient m => ClientM a -> m a
runClient action = do
    clientEnv <- askClientEnv
    liftIO (runClientM action clientEnv) >>= \case
        Left err -> throwM $ ClientError (show err)
        Right a -> return a

convFileMode :: (Integral a, Show a) => a -> FileMode
convFileMode m = FileMode . pad $ showOct m ""
  where pad s | n <- length s, n < 4 = replicate (4 - n) '0' ++ s
              | otherwise            = s

convFileMode' :: (MonadThrow m, Eq a, Num a) => FileMode -> m a
convFileMode' (FileMode fm) = case readOct fm of
    [(m, "")] -> return m
    _ -> throwM $ ClientError $ "received invalid file mode: " ++ show fm

-- | Exception raised when the remote host client couldn't be reached.
newtype ClientError = ClientError String

instance Show ClientError where
    show (ClientError err) = "Could not connect to the LXD host: " ++ err

instance Exception ClientError where

-- | Exception raised when the status of a response was unexpected.
data StatusError = StatusError {
    statusErrorExpected :: StatusCode
  , statusErrorReceived :: StatusCode }

instance Show StatusError where
    show StatusError{..} = "Unexpected response with code "
                        ++ show statusErrorReceived ++ ", expected "
                        ++ show statusErrorExpected

instance Exception StatusError where

-- | Check the validity of a synchronous response.
checkResponseOK :: MonadThrow m => Response a -> m a
checkResponseOK Response{..}
    | SSuccess <- statusCode = return metadata
    | otherwise = throwM $ StatusError SSuccess statusCode

-- | Check the validity of an asynchronous response.
checkResponseCreated :: MonadThrow m => AsyncResponse a -> m OperationId
checkResponseCreated Response{..}
    | SCreated <- statusCode = return responseOperation
    | otherwise = throwM $ StatusError SCreated statusCode

-- | Check the validity of an asynchronous response and return the metadata.
checkResponseCreatedMetadata :: MonadThrow m => AsyncResponse a -> m a
checkResponseCreatedMetadata Response{..}
    | SCreated <- statusCode = return $ backgroundOperationMetadata metadata
    | otherwise = throwM $ StatusError SCreated statusCode

-- | Wait for an operation, and check whether it was successfull
runAndWait :: HasClient m => ClientM OperationId -> m ()
runAndWait op = do
    oid  <- liftIO newEmptyMVar
    ops  <- liftIO newEmptyMVar
    host <- askHost

    let err = throwM . OperationError
    let waitForDone = do op' <- liftIO $ takeMVar ops
                         case operationStatusCode op' of
                             SSuccess   -> return ()
                             SStopped   -> err "Operation unexpectedly stopped"
                             SCancelled -> err "Opeartion unexpectedly cancelled"
                             SFailure   -> err $ "Operation failed: " ++ operationErr op'
                             SRunning   -> printProgress op' >> waitForDone
                             _           -> waitForDone

    bracket
        (liftIO . async . runWebSockets host operationsPath $ listenForOperation oid ops)
        (liftIO . uninterruptibleCancel) $ \_ -> do
            oid' <- runClient op
            liftIO $ putMVar oid oid'
            waitForDone
  where
    printProgress op' = case Aeson.fromJSON (operationMetadata op') of
        Aeson.Error _ -> return ()
        Aeson.Success (OperationProgress p) -> liftIO $ do
            IO.hPutStr IO.stderr $ "Progres: " ++ p ++ "\r"
            IO.hFlush IO.stderr

-- | Exception raised when the operation was unsuccessful.
newtype OperationError = OperationError String

instance Show OperationError where
    show (OperationError err) = "Operation unsuccessful: " ++ err

instance Exception OperationError where

-- $use
--
-- All commands take place in the 'HasClient' monad. The 'WithLocalHost'
-- and 'WithRemoteHost' monads can be used directly for fast access to
-- an LXD daemon, but you can also make your own monad stack an instance
-- of 'HasClient'.
--
-- You can connect to an LXD daemon over a unix-socket on the local
-- host, or over HTTPS. For more information about these connection
-- types see "Network.LXD.Client".
--
-- An example using these command to conncet to the LXD instance on your
-- local host (should work out of the box).
--
-- > {-# LANGUAGE OverloadedStrings #-}
-- > module Main where
-- >
-- > import Control.Monad.IO.Class (liftIO)
-- > import Network.LXD.Client.Commands
-- >
-- > main :: IO ()
-- > main = runWithLocalHost def $ do
-- >     liftIO $ putStrLn "Creating my-container"
-- >     lxcCreate . containerCreateRequest "my-container"
-- >               . ContainerSourceRemote
-- >               $ remoteImage imagesRemote "ubuntu/xenial/amd64"
-- >
-- >     liftIO $ putStrLn "Starting my-container"
-- >     lxcStart "my-container"
-- >
-- >     liftIO $ putStrLn "Stopping my-container"
-- >     lxcStop "my-container" False
-- >
-- >     liftIO $ putStrLn "Deleting my-container"
-- >     lxcDelete "my-container"
--