--
-- Minio Haskell SDK, (C) 2018 Minio, Inc.
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--

module Network.Minio.AdminAPI
  ( -- * Minio Admin API
    --------------------
    -- | Provides Minio admin API and related types. It is in
    -- experimental state.
    DriveInfo(..)
  , ErasureInfo(..)
  , Backend(..)
  , ConnStats(..)
  , HttpStats(..)
  , ServerProps(..)
  , CountNAvgTime(..)
  , StorageClass(..)
  , StorageInfo(..)
  , SIData(..)
  , ServerInfo(..)
  , getServerInfo

  , HealOpts(..)
  , HealResultItem(..)
  , HealStatus(..)
  , HealStartResp(..)
  , startHeal
  , forceStartHeal
  , getHealStatus

  , SetConfigResult(..)
  , NodeSummary(..)
  , setConfig
  , getConfig

  , ServerVersion(..)
  , ServiceStatus(..)
  , serviceStatus

  , ServiceAction(..)
  , serviceSendAction
  ) where

import           Data.Aeson                (FromJSON, ToJSON, Value (Object),
                                            eitherDecode, object, pairs,
                                            parseJSON, toEncoding, toJSON,
                                            withObject, withText, (.:), (.:?),
                                            (.=))
import qualified Data.Aeson                as A
import           Data.Aeson.Types          (typeMismatch)
import qualified Data.ByteString           as B
import qualified Data.ByteString.Lazy      as LBS
import qualified Data.Text                 as T
import           Data.Time                 (NominalDiffTime, getCurrentTime)
import           Network.HTTP.Conduit      (Response)
import qualified Network.HTTP.Conduit      as NC
import qualified Network.HTTP.Types        as HT
import           Network.HTTP.Types.Header (hHost)

import           Lib.Prelude

import           Network.Minio.APICommon
import           Network.Minio.Data
import           Network.Minio.Errors
import           Network.Minio.Sign.V4
import           Network.Minio.Utils

data DriveInfo = DriveInfo
                 { diUuid     :: Text
                 , diEndpoint :: Text
                 , diState    :: Text
                 } deriving (Eq, Show)

instance FromJSON DriveInfo where
    parseJSON = withObject "DriveInfo" $ \v -> DriveInfo
        <$> v .: "uuid"
        <*> v .: "endpoint"
        <*> v .: "state"

data StorageClass = StorageClass
                    { scParity :: Int
                    , scData   :: Int
                    } deriving (Eq, Show)

data ErasureInfo = ErasureInfo
                   { eiOnlineDisks       :: Int
                   , eiOfflineDisks      :: Int
                   , eiStandard          :: StorageClass
                   , eiReducedRedundancy :: StorageClass
                   , eiSets              :: [[DriveInfo]]
                   } deriving (Eq, Show)

instance FromJSON ErasureInfo where
    parseJSON = withObject "ErasureInfo" $ \v -> do
        onlineDisks <- v .: "OnlineDisks"
        offlineDisks <- v .: "OfflineDisks"
        stdClass <- StorageClass
                    <$> v .: "StandardSCData"
                    <*> v .: "StandardSCParity"
        rrClass <- StorageClass
                   <$>  v .: "RRSCData"
                   <*>  v .: "RRSCParity"
        sets <- v .: "Sets"
        return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets

data Backend = BackendFS
             | BackendErasure ErasureInfo
             deriving (Eq, Show)

instance FromJSON Backend where
    parseJSON = withObject "Backend" $ \v -> do
        typ <- v .: "Type"
        case typ :: Int of
            1 -> return BackendFS
            2 -> BackendErasure <$> parseJSON (Object v)
            _ -> typeMismatch "BackendType" (Object v)

data ConnStats = ConnStats
    { csTransferred :: Int64
    , csReceived    :: Int64
    } deriving (Eq, Show)

instance FromJSON ConnStats where
    parseJSON = withObject "ConnStats" $ \v -> ConnStats
        <$> v .: "transferred"
        <*> v .: "received"

data ServerProps = ServerProps
    { spUptime   :: NominalDiffTime
    , spVersion  :: Text
    , spCommitId :: Text
    , spRegion   :: Text
    , spSqsArns  :: [Text]
    } deriving (Eq, Show)

instance FromJSON ServerProps where
    parseJSON = withObject "SIServer" $ \v -> do
        uptimeNs <- v .: "uptime"
        let uptime = uptimeNs / 1e9
        ver <- v .: "version"
        commitId <- v .: "commitID"
        region <- v .: "region"
        arn <- v .: "sqsARN"
        return $ ServerProps uptime ver commitId region arn

data StorageInfo = StorageInfo
    { siUsed    :: Int64
    , siBackend :: Backend
    } deriving (Eq, Show)

instance FromJSON StorageInfo where
    parseJSON = withObject "StorageInfo" $ \v -> StorageInfo
        <$> v .: "Used"
        <*> v .: "Backend"

data CountNAvgTime = CountNAvgTime
    {  caCount       :: Int64
    ,  caAvgDuration :: Text
    } deriving (Eq, Show)

instance FromJSON CountNAvgTime where
    parseJSON = withObject "CountNAvgTime" $ \v -> CountNAvgTime
        <$> v .: "count"
        <*> v .: "avgDuration"

data HttpStats = HttpStats
    { hsTotalHeads     :: CountNAvgTime
    , hsSuccessHeads   :: CountNAvgTime
    , hsTotalGets      :: CountNAvgTime
    , hsSuccessGets    :: CountNAvgTime
    , hsTotalPuts      :: CountNAvgTime
    , hsSuccessPuts    :: CountNAvgTime
    , hsTotalPosts     :: CountNAvgTime
    , hsSuccessPosts   :: CountNAvgTime
    , hsTotalDeletes   :: CountNAvgTime
    , hsSuccessDeletes :: CountNAvgTime
    } deriving (Eq, Show)

instance FromJSON HttpStats where
    parseJSON  = withObject "HttpStats" $ \v -> HttpStats
        <$> v .: "totalHEADs"
        <*> v .: "successHEADs"
        <*> v .: "totalGETs"
        <*> v .: "successGETs"
        <*> v .: "totalPUTs"
        <*> v .: "successPUTs"
        <*> v .: "totalPOSTs"
        <*> v .: "successPOSTs"
        <*> v .: "totalDELETEs"
        <*> v .: "successDELETEs"

data SIData = SIData
    { sdStorage   :: StorageInfo
    , sdConnStats :: ConnStats
    , sdHttpStats :: HttpStats
    , sdProps     :: ServerProps
    } deriving (Eq, Show)

instance FromJSON SIData where
    parseJSON = withObject "SIData" $ \v -> SIData
        <$> v .: "storage"
        <*> v .: "network"
        <*> v .: "http"
        <*> v .: "server"

data ServerInfo = ServerInfo
    { siError :: Text
    , siAddr  :: Text
    , siData  :: SIData
    } deriving (Eq, Show)

instance FromJSON ServerInfo where
    parseJSON = withObject "ServerInfo" $ \v -> ServerInfo
        <$> v .: "error"
        <*> v .: "addr"
        <*> v .: "data"

data ServerVersion = ServerVersion
  { svVersion  :: Text
  , svCommitId :: Text
  } deriving (Eq, Show)

instance FromJSON ServerVersion where
    parseJSON = withObject "ServerVersion" $ \v -> ServerVersion
      <$> v .: "version"
      <*> v .: "commitID"

data ServiceStatus = ServiceStatus
  { ssVersion :: ServerVersion
  , ssUptime  :: NominalDiffTime
  } deriving (Eq, Show)

instance FromJSON ServiceStatus where
    parseJSON = withObject "ServiceStatus" $ \v -> do
      serverVersion <- v .: "serverVersion"
      uptimeNs <- v .: "uptime"
      let uptime = uptimeNs / 1e9
      return $ ServiceStatus serverVersion uptime

data ServiceAction = ServiceActionRestart
                   | ServiceActionStop
                   deriving (Eq, Show)

instance ToJSON ServiceAction where
    toJSON a = object [ "action" .= serviceActionToText a ]

serviceActionToText :: ServiceAction -> Text
serviceActionToText a = case a of
  ServiceActionRestart -> "restart"
  ServiceActionStop    -> "stop"

adminPath :: ByteString
adminPath = "/minio/admin"

data HealStartResp = HealStartResp
  { hsrClientToken :: Text
  , hsrClientAddr  :: Text
  , hsrStartTime   :: UTCTime
  } deriving (Eq, Show)

instance FromJSON HealStartResp where
    parseJSON = withObject "HealStartResp" $ \v -> HealStartResp
        <$> v .: "clientToken"
        <*> v .: "clientAddress"
        <*> v .: "startTime"

data HealOpts = HealOpts
  { hoRecursive :: Bool
  , hoDryRun    :: Bool
  } deriving (Eq, Show)

instance ToJSON HealOpts where
  toJSON (HealOpts r d) =
    object ["recursive" .= r, "dryRun" .= d]
  toEncoding (HealOpts r d) =
    pairs ("recursive" .= r <> "dryRun" .= d)

instance FromJSON HealOpts where
    parseJSON = withObject "HealOpts" $ \v -> HealOpts
      <$> v .: "recursive"
      <*> v .: "dryRun"

data HealItemType = HealItemMetadata
                  | HealItemBucket
                  | HealItemBucketMetadata
                  | HealItemObject
                  deriving (Eq, Show)

instance FromJSON HealItemType where
    parseJSON = withText "HealItemType" $ \v -> case v of
      "metadata"        -> return HealItemMetadata
      "bucket"          -> return HealItemBucket
      "object"          -> return HealItemObject
      "bucket-metadata" -> return HealItemBucketMetadata
      _                 -> typeMismatch "HealItemType" (A.String v)

data NodeSummary = NodeSummary
  { nsName       :: Text
  , nsErrSet     :: Bool
  , nsErrMessage :: Text
  } deriving (Eq, Show)

instance FromJSON NodeSummary where
  parseJSON = withObject "NodeSummary" $ \v -> NodeSummary
    <$> v .: "name"
    <*> v .: "errSet"
    <*> v .: "errMsg"

data SetConfigResult = SetConfigResult
  { scrStatus      :: Bool
  , scrNodeSummary :: [NodeSummary]
  } deriving (Eq, Show)

instance FromJSON SetConfigResult where
  parseJSON = withObject "SetConfigResult" $ \v -> SetConfigResult
    <$> v .: "status"
    <*> v .: "nodeResults"

data HealResultItem = HealResultItem
  { hriResultIdx    :: Int
  , hriType         :: HealItemType
  , hriBucket       :: Bucket
  , hriObject       :: Object
  , hriDetail       :: Text
  , hriParityBlocks :: Maybe Int
  , hriDataBlocks   :: Maybe Int
  , hriDiskCount    :: Int
  , hriSetCount     :: Int
  , hriObjectSize   :: Int
  , hriBefore       :: [DriveInfo]
  , hriAfter        :: [DriveInfo]
  } deriving (Eq, Show)

instance FromJSON HealResultItem where
  parseJSON = withObject "HealResultItem" $ \v -> HealResultItem
    <$> v .: "resultId"
    <*> v .: "type"
    <*> v .: "bucket"
    <*> v .: "object"
    <*> v .: "detail"
    <*> v .:? "parityBlocks"
    <*> v .:? "dataBlocks"
    <*> v .: "diskCount"
    <*> v .: "setCount"
    <*> v .: "objectSize"
    <*> (do before <- v .: "before"
            before .: "drives")
    <*> (do after <- v .: "after"
            after .: "drives")

data HealStatus = HealStatus
  { hsSummary       :: Text
  , hsStartTime     :: UTCTime
  , hsSettings      :: HealOpts
  , hsNumDisks      :: Int
  , hsFailureDetail :: Maybe Text
  , hsItems         :: Maybe [HealResultItem]
  } deriving (Eq, Show)

instance FromJSON HealStatus where
  parseJSON = withObject "HealStatus" $ \v -> HealStatus
    <$> v .: "Summary"
    <*> v .: "StartTime"
    <*> v .: "Settings"
    <*> v .: "NumDisks"
    <*> v .:? "Detail"
    <*> v .: "Items"

healPath :: Maybe Bucket -> Maybe Text -> ByteString
healPath bucket prefix = do
  if (isJust bucket)
    then encodeUtf8 $ "v1/heal/" <> fromMaybe "" bucket <> "/"
         <> fromMaybe "" prefix
    else encodeUtf8 $ "v1/heal/"

-- | Get server version and uptime.
serviceStatus :: Minio ServiceStatus
serviceStatus = do
    rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
                                            , ariPayload = PayloadBS B.empty
                                            , ariPayloadHash = Nothing
                                            , ariPath = "v1/service"
                                            , ariHeaders = []
                                            , ariQueryParams = []
                                            }

    let rspBS = NC.responseBody rsp
    case eitherDecode rspBS of
        Right ss -> return ss
        Left err -> throwIO $ MErrVJsonParse $ T.pack err

-- | Send service restart or stop action to Minio server.
serviceSendAction :: ServiceAction -> Minio ()
serviceSendAction action = do
    let payload = PayloadBS $ LBS.toStrict $ A.encode action
    void $ executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
                                            , ariPayload = payload
                                            , ariPayloadHash = Nothing
                                            , ariPath = "v1/service"
                                            , ariHeaders = []
                                            , ariQueryParams = []
                                            }

-- | Get the current config file from server.
getConfig :: Minio ByteString
getConfig = do
    rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
                                            , ariPayload = PayloadBS B.empty
                                            , ariPayloadHash = Nothing
                                            , ariPath = "v1/config"
                                            , ariHeaders = []
                                            , ariQueryParams = []
                                            }
    return $ LBS.toStrict $ NC.responseBody rsp

-- | Set a new config to the server.
setConfig :: ByteString -> Minio SetConfigResult
setConfig config = do
    rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPut
                                            , ariPayload = PayloadBS config
                                            , ariPayloadHash = Nothing
                                            , ariPath = "v1/config"
                                            , ariHeaders = []
                                            , ariQueryParams = []
                                            }

    let rspBS = NC.responseBody rsp
    case eitherDecode rspBS of
        Right scr -> return scr
        Left err  -> throwIO $ MErrVJsonParse $ T.pack err

-- | Get the progress of currently running heal task, this API should be
-- invoked right after `startHeal`. `token` is obtained after `startHeal`
-- which should be used to get the heal status.
getHealStatus :: Maybe Bucket -> Maybe Text -> Text -> Minio HealStatus
getHealStatus bucket prefix token = do
    when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
    let qparams = HT.queryTextToQuery [("clientToken", Just token)]
    rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
                                            , ariPayload = PayloadBS B.empty
                                            , ariPayloadHash = Nothing
                                            , ariPath = healPath bucket prefix
                                            , ariHeaders = []
                                            , ariQueryParams = qparams
                                            }
    let rspBS = NC.responseBody rsp
    case eitherDecode rspBS of
        Right hs -> return hs
        Left err -> throwIO $ MErrVJsonParse $ T.pack err

doHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Bool -> Minio HealStartResp
doHeal bucket prefix opts forceStart = do
    when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
    let payload = PayloadBS $ LBS.toStrict $ A.encode opts
    let qparams = bool [] (HT.queryTextToQuery [("forceStart", Just "true")])
                  forceStart

    rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
                                            , ariPayload = payload
                                            , ariPayloadHash = Nothing
                                            , ariPath = healPath bucket prefix
                                            , ariHeaders = []
                                            , ariQueryParams = qparams
                                            }

    let rspBS = NC.responseBody rsp
    case eitherDecode rspBS of
        Right hsr -> return hsr
        Left err  -> throwIO $ MErrVJsonParse $ T.pack err

-- | Start a heal sequence that scans data under given (possible empty)
-- `bucket` and `prefix`. The `recursive` bool turns on recursive
-- traversal under the given path. `dryRun` does not mutate on-disk data,
-- but performs data validation. Two heal sequences on overlapping paths
-- may not be initiated. The progress of a heal should be followed using
-- the `HealStatus` API. The server accumulates results of the heal
-- traversal and waits for the client to receive and acknowledge
-- them using the status API
startHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Minio HealStartResp
startHeal bucket prefix opts = doHeal bucket prefix opts False

-- | Similar to start a heal sequence, but force start a new heal sequence
-- even if an active heal is under progress.
forceStartHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Minio HealStartResp
forceStartHeal bucket prefix opts = doHeal bucket prefix opts True

-- | Fetches information for all cluster nodes, such as server
-- properties, storage information, network statistics, etc.
getServerInfo :: Minio [ServerInfo]
getServerInfo = do
    rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
                                            , ariPayload = PayloadBS B.empty
                                            , ariPayloadHash = Nothing
                                            , ariPath = "v1/info"
                                            , ariHeaders = []
                                            , ariQueryParams = []
                                            }
    let rspBS = NC.responseBody rsp
    case eitherDecode rspBS of
        Right si -> return si
        Left err -> throwIO $ MErrVJsonParse $ T.pack err

executeAdminRequest :: AdminReqInfo -> Minio (Response LByteString)
executeAdminRequest ari = do
    req <- buildAdminRequest ari
    mgr <- asks mcConnManager
    httpLbs req mgr

buildAdminRequest :: AdminReqInfo -> Minio NC.Request
buildAdminRequest areq = do
    ci <- asks mcConnInfo
    sha256Hash <- if | connectIsSecure ci ->
                       -- if secure connection
                       return "UNSIGNED-PAYLOAD"

                       -- otherwise compute sha256
                     | otherwise -> getPayloadSHA256Hash (ariPayload areq)

    timeStamp <- liftIO getCurrentTime

    let hostHeader = (hHost, getHostAddr ci)
        newAreq = areq { ariPayloadHash = Just sha256Hash
                       , ariHeaders = hostHeader
                                    : sha256Header sha256Hash
                                    : ariHeaders areq
                       }
        signReq = toRequest ci newAreq
        sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
             timeStamp Nothing Nothing (ariPayloadHash newAreq)
        signHeaders = signV4 sp signReq

    -- Update signReq with Authorization header containing v4 signature
    return signReq {
        NC.requestHeaders = ariHeaders newAreq ++ mkHeaderFromPairs signHeaders
        }
  where
    toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
    toRequest ci aReq = NC.defaultRequest
        { NC.method = ariMethod aReq
        , NC.secure = connectIsSecure ci
        , NC.host = encodeUtf8 $ connectHost ci
        , NC.port = connectPort ci
        , NC.path = B.intercalate "/" [adminPath, ariPath aReq]
        , NC.requestHeaders = ariHeaders aReq
        , NC.queryString = HT.renderQuery False $ ariQueryParams aReq
        , NC.requestBody = getRequestBody (ariPayload aReq)
        }