{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeOperators #-}
-- | This module exposes a low-level Servant-built API for the LXD daemon.
--
-- You can query this API using Servant functions and a client created
-- from "Network.LXD.Client".
--
-- You probably want to use the commands exposed in
-- "Network.LXD.Client.Commands" instead.
module Network.LXD.Client.API (
  -- * API
  FailureResponse(..)
  -- ** Information
, supportedVersions
, apiConfig
, trustedCertificates

  -- ** Containers
  -- *** Querying informaiton
, containerNames
, containerCreate
, container
, containerDelete
  -- *** Configuration
, containerPut
, containerPatch
, containerRename
  -- *** State
, containerState
, containerPutState
  -- *** Executing commands
, containerExecImmediate
, containerExecWebsocketInteractive
, containerExecWebsocketNonInteractive
  -- *** Working with files
, WriteMode(..)
, containerGetPath
, containerPostPath
, containerDeletePath

  -- ** Images
, imageIds
, imageCreate
, imageAliases
, imageAlias
, image
, imageDelete

  -- ** Networks
, networkList
, networkCreate
, network
, networkPut
, networkPatch
, networkDelete

  -- ** Profiles
, profileList
, profileCreate
, profile
, profilePut
, profilePatch
, profileDelete

  -- ** Storage
, poolList
, poolCreate
, pool
, poolPut
, poolPatch
, poolDelete

  -- * Volumes
, volumeList
, volumeCreate
, volume
, volumePut
, volumePatch
, volumeDelete

  -- ** Operations
, operationIds
, operation
, operationCancel
, operationWait
, operationWebSocket

  -- ** WebSocket communciations
, readAllWebSocket
, writeAllWebSocket

  -- * Helpers
, ExecClient
) where

import Network.LXD.Client.Internal.Prelude

import Control.Concurrent (MVar, takeMVar)
import Control.Exception (Exception, catch, throwIO)
import Control.Monad.Reader (asks)

import Data.Aeson (FromJSON, Value, eitherDecode)
import Data.ByteString.Lazy (ByteString, toStrict)
import Data.List (find)
import Data.Maybe (catMaybes)
import Data.Proxy
import Data.String (fromString)
import Data.Text (Text)
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as Char8

import qualified Network.HTTP.Client as Client
import qualified Network.HTTP.Types.Header as HTTP
import qualified Network.HTTP.Types.Status as HTTP
import qualified Network.WebSockets as WS

import Servant.API

#if MIN_VERSION_servant(0, 12, 0)
import Servant.Client hiding (Response, FailureResponse)
#else
import Servant.Client hiding (FailureResponse)
#endif

import Web.HttpApiData (FromHttpApiData, ToHttpApiData, toHeader, parseHeader)

import Network.LXD.Client.Types
import Network.LXD.Client.Internal.Compatibility (compat)
import qualified Network.LXD.Client.Internal.Compatibility.WebSockets as WSC


type API = Get '[JSON] (Response [ApiVersion])
      :<|> "1.0" :> Get '[JSON] (Response ApiConfig)
      :<|> "1.0" :> "certificates" :> Get '[JSON] (Response [CertificateHash])
      :<|> "1.0" :> "containers" :> Get '[JSON] (Response [ContainerName])
      :<|> "1.0" :> "containers" :> ReqBody '[JSON] ContainerCreateRequest :> Post '[JSON] (AsyncResponse Value)
      :<|> "1.0" :> "containers" :> Capture "name" ContainerName :> Get '[JSON] (Response Container)
      :<|> "1.0" :> "containers" :> Capture "name" ContainerName :> ReqBody '[JSON] ContainerDeleteRequest :> Delete '[JSON] (AsyncResponse Value)
      :<|> "1.0" :> "containers" :> Capture "name" ContainerName :> ReqBody '[JSON] ContainerPut :> Put '[JSON] (AsyncResponse Value)
      :<|> "1.0" :> "containers" :> Capture "name" ContainerName :> ReqBody '[JSON] ContainerPatch :> Patch '[JSON] (Response Value)
      :<|> "1.0" :> "containers" :> Capture "name" ContainerName :> ReqBody '[JSON] ContainerRename :> Post '[JSON] (AsyncResponse Value)
      :<|> "1.0" :> "containers" :> Capture "name" ContainerName :> "state" :> Get '[JSON] (Response ContainerState)
      :<|> "1.0" :> "containers" :> Capture "name" ContainerName :> "state" :> ReqBody '[JSON] ContainerPutState :> Put '[JSON] (AsyncResponse Value)
      :<|> ExecAPI 'ExecImmediate
      :<|> ExecAPI 'ExecWebsocketInteractive
      :<|> ExecAPI 'ExecWebsocketNonInteractive
      :<|> "1.0" :> "images" :> Get '[JSON] (Response [ImageId])
      :<|> "1.0" :> "images" :> ReqBody '[JSON] ImageCreateRequest :> Post '[JSON] (AsyncResponse Value)
      :<|> "1.0" :> "images" :> "aliases" :> Get '[JSON] (Response [ImageAliasName])
      :<|> "1.0" :> "images" :> "aliases" :> Capture "name" ImageAliasName :> Get '[JSON] (Response ImageAlias)
      :<|> "1.0" :> "images" :> Capture "id" ImageId :> Get '[JSON] (Response Image)
      :<|> "1.0" :> "images" :> Capture "id" ImageId :> ReqBody '[JSON] ImageDeleteRequest :> Delete '[JSON] (AsyncResponse Value)
      :<|> "1.0" :> "networks" :> Get '[JSON] (Response [NetworkName])
      :<|> "1.0" :> "networks" :> ReqBody '[JSON] NetworkCreateRequest :> Post '[JSON] (Response Value)
      :<|> "1.0" :> "networks" :> Capture "name" NetworkName :> Get '[JSON] (Response Network)
      :<|> "1.0" :> "networks" :> Capture "name" NetworkName :> ReqBody '[JSON] NetworkConfigRequest :> Put '[JSON] (Response Value)
      :<|> "1.0" :> "networks" :> Capture "name" NetworkName :> ReqBody '[JSON] NetworkConfigRequest :> Patch '[JSON] (Response Value)
      :<|> "1.0" :> "networks" :> Capture "name" NetworkName :> Delete '[JSON] (Response Value)
      :<|> "1.0" :> "profiles" :> Get '[JSON] (Response [ProfileName])
      :<|> "1.0" :> "profiles" :> ReqBody '[JSON] ProfileCreateRequest :> Post '[JSON] (Response Value)
      :<|> "1.0" :> "profiles" :> Capture "name" ProfileName :> Get '[JSON] (Response Profile)
      :<|> "1.0" :> "profiles" :> Capture "name" ProfileName :> ReqBody '[JSON] ProfileConfigRequest :> Put '[JSON] (Response Value)
      :<|> "1.0" :> "profiles" :> Capture "name" ProfileName :> ReqBody '[JSON] ProfileConfigRequest :> Patch '[JSON] (Response Value)
      :<|> "1.0" :> "profiles" :> Capture "name" ProfileName :> Delete '[JSON] (Response Value)
      :<|> "1.0" :> "storage-pools" :> Get '[JSON] (Response [PoolName])
      :<|> "1.0" :> "storage-pools" :> ReqBody '[JSON] PoolCreateRequest :> Post '[JSON] (Response Value)
      :<|> "1.0" :> "storage-pools" :> Capture "name" PoolName :> Get '[JSON] (Response Pool)
      :<|> "1.0" :> "storage-pools" :> Capture "name" PoolName :> ReqBody '[JSON] PoolConfigRequest :> Put '[JSON] (Response Value)
      :<|> "1.0" :> "storage-pools" :> Capture "name" PoolName :> ReqBody '[JSON] PoolConfigRequest :> Patch '[JSON] (Response Value)
      :<|> "1.0" :> "storage-pools" :> Capture "name" PoolName :> Delete '[JSON] (Response Value)
      :<|> "1.0" :> "storage-pools" :> Capture "pool" PoolName :> "volumes" :> Get '[JSON] (Response [VolumeName])
      :<|> "1.0" :> "storage-pools" :> Capture "pool" PoolName :> "volumes" :> ReqBody '[JSON] VolumeCreateRequest :> Post '[JSON] (Response Value)
      :<|> "1.0" :> "storage-pools" :> Capture "pool" PoolName :> "volumes" :> Capture "type" VolumeType :> Capture "volume" VolumeName :> Get '[JSON] (Response Volume)
      :<|> "1.0" :> "storage-pools" :> Capture "pool" PoolName :> "volumes" :> Capture "type" VolumeType :> Capture "volume" VolumeName :> ReqBody '[JSON] VolumeConfigRequest :> Put '[JSON] (Response Value)
      :<|> "1.0" :> "storage-pools" :> Capture "pool" PoolName :> "volumes" :> Capture "type" VolumeType :> Capture "volume" VolumeName :> ReqBody '[JSON] VolumeConfigRequest :> Patch '[JSON] (Response Value)
      :<|> "1.0" :> "storage-pools" :> Capture "pool" PoolName :> "volumes" :> Capture "type" VolumeType :> Capture "volume" VolumeName :> Delete '[JSON] (Response Value)
      :<|> "1.0" :> "operations" :> Get '[JSON] (Response AllOperations)
      :<|> "1.0" :> "operations" :> Capture "uuid" OperationId :> Get '[JSON] (Response Operation)
      :<|> "1.0" :> "operations" :> Capture "uuid" OperationId :> Delete '[JSON] (Response Value)
      :<|> "1.0" :> "operations" :> Capture "uuid" OperationId :> "wait" :> Get '[JSON] (Response Operation)


api :: Proxy API
api = Proxy

supportedVersions                    :: ClientM (Response [ApiVersion])
apiConfig                            :: ClientM (Response ApiConfig)
trustedCertificates                  :: ClientM (Response [CertificateHash])
containerNames                       :: ClientM (Response [ContainerName])
containerCreate                      :: ContainerCreateRequest -> ClientM (AsyncResponse Value)
container                            :: ContainerName -> ClientM (Response Container)
containerDelete                      :: ContainerName -> ContainerDeleteRequest -> ClientM (AsyncResponse Value)
containerPut                         :: ContainerName -> ContainerPut -> ClientM (AsyncResponse Value)
containerPatch                       :: ContainerName -> ContainerPatch -> ClientM (Response Value)
containerRename                      :: ContainerName -> ContainerRename -> ClientM (AsyncResponse Value)
containerState                       :: ContainerName -> ClientM (Response ContainerState)
containerPutState                    :: ContainerName -> ContainerPutState -> ClientM (AsyncResponse Value)
containerExecImmediate               :: ExecClient 'ExecImmediate
containerExecWebsocketInteractive    :: ExecClient 'ExecWebsocketInteractive
containerExecWebsocketNonInteractive :: ExecClient 'ExecWebsocketNonInteractive
imageIds                             :: ClientM (Response [ImageId])
imageCreate                          :: ImageCreateRequest -> ClientM (AsyncResponse Value)
imageAliases                         :: ClientM (Response [ImageAliasName])
imageAlias                           :: ImageAliasName -> ClientM (Response ImageAlias)
image                                :: ImageId -> ClientM (Response Image)
imageDelete                          :: ImageId -> ImageDeleteRequest -> ClientM (AsyncResponse Value)
networkList                          :: ClientM (Response [NetworkName])
networkCreate                        :: NetworkCreateRequest -> ClientM (Response Value)
network                              :: NetworkName -> ClientM (Response Network)
networkPut                           :: NetworkName -> NetworkConfigRequest -> ClientM (Response Value)
networkPatch                         :: NetworkName -> NetworkConfigRequest -> ClientM (Response Value)
networkDelete                        :: NetworkName -> ClientM (Response Value)
profileList                          :: ClientM (Response [ProfileName])
profileCreate                        :: ProfileCreateRequest -> ClientM (Response Value)
profile                              :: ProfileName -> ClientM (Response Profile)
profilePut                           :: ProfileName -> ProfileConfigRequest -> ClientM (Response Value)
profilePatch                         :: ProfileName -> ProfileConfigRequest -> ClientM (Response Value)
profileDelete                        :: ProfileName -> ClientM (Response Value)
poolList                             :: ClientM (Response [PoolName])
poolCreate                           :: PoolCreateRequest -> ClientM (Response Value)
pool                                 :: PoolName -> ClientM (Response Pool)
poolPut                              :: PoolName -> PoolConfigRequest -> ClientM (Response Value)
poolPatch                            :: PoolName -> PoolConfigRequest -> ClientM (Response Value)
poolDelete                           :: PoolName -> ClientM (Response Value)
volumeList                           :: PoolName -> ClientM (Response [VolumeName])
volumeCreate                         :: PoolName -> VolumeCreateRequest -> ClientM (Response Value)
volume'                              :: PoolName -> VolumeType -> VolumeName -> ClientM (Response Volume)
volumePut'                           :: PoolName -> VolumeType -> VolumeName -> VolumeConfigRequest -> ClientM (Response Value)
volumePatch'                         :: PoolName -> VolumeType -> VolumeName -> VolumeConfigRequest -> ClientM (Response Value)
volumeDelete'                        :: PoolName -> VolumeType -> VolumeName -> ClientM (Response Value)
operationIds                         :: ClientM (Response AllOperations)
operation                            :: OperationId -> ClientM (Response Operation)
operationCancel                      :: OperationId -> ClientM (Response Value)
operationWait                        :: OperationId -> ClientM (Response Operation)

supportedVersions                        :<|>
    apiConfig                            :<|>
    trustedCertificates                  :<|>
    containerNames                       :<|>
    containerCreate                      :<|>
    container                            :<|>
    containerDelete                      :<|>
    containerPut                         :<|>
    containerPatch                       :<|>
    containerRename                      :<|>
    containerState                       :<|>
    containerPutState                    :<|>
    containerExecImmediate               :<|>
    containerExecWebsocketInteractive    :<|>
    containerExecWebsocketNonInteractive :<|>
    imageIds                             :<|>
    imageCreate                          :<|>
    imageAliases                         :<|>
    imageAlias                           :<|>
    image                                :<|>
    imageDelete                          :<|>
    networkList                          :<|>
    networkCreate                        :<|>
    network                              :<|>
    networkPut                           :<|>
    networkPatch                         :<|>
    networkDelete                        :<|>
    profileList                          :<|>
    profileCreate                        :<|>
    profile                              :<|>
    profilePut                           :<|>
    profilePatch                         :<|>
    profileDelete                        :<|>
    poolList                             :<|>
    poolCreate                           :<|>
    pool                                 :<|>
    poolPut                              :<|>
    poolPatch                            :<|>
    poolDelete                           :<|>
    volumeList                           :<|>
    volumeCreate                         :<|>
    volume'                              :<|>
    volumePut'                           :<|>
    volumePatch'                         :<|>
    volumeDelete'                        :<|>
    operationIds                         :<|>
    operation                            :<|>
    operationCancel                      :<|>
    operationWait
    = client api

volume :: PoolName -> VolumeName -> ClientM (Response Volume)
volume p v@(VolumeName t _) = volume' p t v

volumePut :: PoolName -> VolumeName -> VolumeConfigRequest -> ClientM (Response Value)
volumePut p v@(VolumeName t _) = volumePut' p t v

volumePatch :: PoolName -> VolumeName -> VolumeConfigRequest -> ClientM (Response Value)
volumePatch p v@(VolumeName t _) = volumePatch' p t v

volumeDelete :: PoolName -> VolumeName -> ClientM (Response Value)
volumeDelete p v@(VolumeName t _) = volumeDelete' p t v

containerGetPath :: ContainerName -> FilePath -> ClientM PathResponse
containerGetPath name fp = do
    req <- pathRequest name fp
    res <- performRequest req
    let lookupHdr :: FromHttpApiData a => HTTP.HeaderName -> ClientM a
        lookupHdr n = lookupHdr' n (Client.responseHeaders res)

    hType <- lookupHdr "X-LXD-Type"
    hUid  <- lookupHdr "X-LXD-Uid"
    hGid  <- lookupHdr "X-LXD-Gid"
    hMode <- lookupHdr "X-LXD-Mode"
    case fileResponse hType (Client.responseBody res) of
        Left err -> error' $ "Could not decode " ++ show hType ++ ": " ++ err
        Right file -> return PathResponse {
                                  pathUid = hUid
                                , pathGid = hGid
                                , pathMode = hMode
                                , pathType = hType
                                , getFile = file
                                }
  where
    lookupHdr' :: FromHttpApiData a => HTTP.HeaderName -> [HTTP.Header] -> ClientM a
    lookupHdr' n xs = case find ((== n) . fst) xs of
        Nothing -> error' $ "Missing header in response: " ++ show n
        Just (_, v) -> case parseHeader v of
            Left err -> error' $ "Could not decode header " ++ show n
                              ++ " with value " ++ show v ++ ": " ++ show err
            Right v' -> return v'

    error' = liftIO . throwIO . DecodeError

data WriteMode = ModeOverwrite | ModeAppend deriving (Show)

containerPostPath :: ContainerName
                  -> FilePath
                  -> Maybe Uid
                  -> Maybe Gid
                  -> Maybe FileMode
                  -> FileType
                  -> Maybe WriteMode
                  -> ByteString
                  -> ClientM (Response Value)
containerPostPath name fp uid gid perm ftype mode body = do
    req <- pathRequest name fp
    let hdrs = catMaybes [ hdr "X-LXD-Uid" <$> uid
                         , hdr "X-LXD-Gid" <$> gid
                         , hdr "X-LXD-Mode" <$> perm
                         , hdr "X-LXD-Type" <$> Just ftype
                         , hdr "X-LXD-Write" . mode' <$> mode ]
    let req' = req { Client.requestHeaders = hdrs
                   , Client.requestBody = Client.RequestBodyLBS body }

    performJsonRequest req'
  where
    mode' ModeOverwrite = "overwrite" :: Text
    mode' ModeAppend    = "append"

    hdr :: ToHttpApiData v => HTTP.HeaderName -> v -> HTTP.Header
    hdr n v = (n, toHeader v)

containerDeletePath :: ContainerName -> FilePath -> ClientM (Response Value)
containerDeletePath name fp = do
    res <- pathRequest name fp
    performJsonRequest $ res { Client.method = "DELETE" }

operationWebSocket :: OperationId -> Secret -> String
operationWebSocket (OperationId oid) (Secret secret) =
    "/1.0/operations/" ++ oid ++ "/websocket?secret=" ++ secret

readAllWebSocket :: (ByteString -> IO ()) -> WS.ClientApp ()
readAllWebSocket f con = do
    m <- (Just . compat <$> WS.receiveDataMessage con) `catch` handle'
    case m of Nothing              -> return ()
              Just (WSC.Text _)    -> WS.sendClose con BL.empty
              Just (WSC.Binary bs) -> f bs
                                   >> readAllWebSocket f con
  where
    handle' (WS.CloseRequest _ _) = return Nothing
    handle' e                     = throwIO e

writeAllWebSocket :: MVar (Maybe ByteString) -> WS.ClientApp ()
writeAllWebSocket input con = do
    i <- takeMVar input
    case i of
        Nothing -> WS.sendClose con BL.empty
        Just bs -> WS.sendBinaryData con bs
                >> writeAllWebSocket input con

type ExecAPI a = "1.0" :> "containers" :> Capture "name" ContainerName :> "exec" :> ReqBody '[JSON] (ExecRequest a) :> Post '[JSON] (AsyncResponse (ExecResponseMetadata a))

type ExecClient a = ContainerName -> ExecRequest a -> ClientM (AsyncResponse (ExecResponseMetadata a))

pathRequest :: ContainerName -> FilePath -> ClientM Client.Request
pathRequest (ContainerName name) fp = do
    BaseUrl{..} <- askBaseUrl
    return Client.defaultRequest {
        Client.method = "GET"
      , Client.host = fromString baseUrlHost
      , Client.port = baseUrlPort
      , Client.path = toStrict $ fromString baseUrlPath <> "/1.0/containers/" <> Char8.pack name <> "/files"
      , Client.queryString = toStrict $ "?path=" <> Char8.pack fp
      }

performRequest :: Client.Request -> ClientM (Client.Response ByteString)
performRequest req = do
    m <- askManager
    r <- liftIO $ Client.httpLbs req m

    let status = Client.responseStatus r
        statusCode' = HTTP.statusCode status
    unless (statusCode' >= 200 && statusCode' < 300) $
        liftIO . throwIO $ FailureResponse req r
    return r

performJsonRequest :: FromJSON a => Client.Request -> ClientM a
performJsonRequest req = do
    res <- performRequest req
    case eitherDecode (Client.responseBody res) of
        Left err -> liftIO . throwIO . DecodeError $ "Could not decode JSON body: " ++ err
        Right v -> return v

-- | Exception thrown in 'containerGetPath', 'containerDeletePath' and
-- 'containerPostPath'.
data FailureResponse = FailureResponse Client.Request (Client.Response ByteString) deriving (Show)

instance Exception FailureResponse where

-- | Exception thrown in 'containerGetPath', 'containerDeletePath' and
-- 'containerPostPath'.
newtype DecodeError = DecodeError String deriving (Show)

instance Exception DecodeError where

askBaseUrl :: ClientM BaseUrl
askBaseUrl = asks $ \(ClientEnv _ url) -> url

askManager :: ClientM Client.Manager
askManager = asks $ \(ClientEnv mgr _) -> mgr