{-# LANGUAGE OverloadedStrings #-}
{-|
Module      : Instana.SDK.Internal.AgentConnection.Announce
Description : Handles the announce phase for establishing the connection to the
agent.
-}
module Instana.SDK.Internal.AgentConnection.Announce
    ( announce
    ) where


import qualified Control.Concurrent.STM                                     as STM
import           Control.Exception                                          (SomeException,
                                                                             catch)
import           Data.Aeson                                                 ((.=))
import qualified Data.Aeson                                                 as Aeson
import           Data.ByteString.Lazy                                       (ByteString)
import           Data.Maybe                                                 (isJust)
import qualified Network.HTTP.Client                                        as HTTP
import           System.Log.Logger                                          (debugM,
                                                                             warningM)
import qualified System.Posix.Files                                         as PosixFiles

import           Instana.SDK.Internal.AgentConnection.AgentReady            as AgentReady
import           Instana.SDK.Internal.AgentConnection.Json.AnnounceResponse (AnnounceResponse)
import           Instana.SDK.Internal.AgentConnection.Paths
import           Instana.SDK.Internal.AgentConnection.ProcessInfo           (ProcessInfo)
import qualified Instana.SDK.Internal.AgentConnection.ProcessInfo           as ProcessInfo
import           Instana.SDK.Internal.Context                               (ConnectionState (..),
                                                                             InternalContext)
import qualified Instana.SDK.Internal.Context                               as InternalContext
import           Instana.SDK.Internal.Logging                               (instanaLogger)
import qualified Instana.SDK.Internal.Retry                                 as Retry
import qualified Instana.SDK.Internal.URL                                   as URL


-- |Starts the announce phase.
announce ::
  InternalContext
  -> ProcessInfo
  -> IO ()
announce context processInfo = do
  debugM instanaLogger "Starting to announce process to agent."
  connectionState <-
      STM.atomically $ STM.readTVar $ InternalContext.connectionState context
  case connectionState of
    Unannounced hostAndPort ->
      announceToAgent context hostAndPort processInfo
    _ -> do
      warningM instanaLogger $
        "Reached illegal state in announce, agent host lookup did not " ++
        "yield a host and port. Connection state is " ++ show connectionState ++
        ". Will retry later."
      STM.atomically $ STM.writeTVar
        (InternalContext.connectionState context)
        Unconnected
      return ()


announceToAgent ::
  InternalContext
  -> (String, Int)
  -> ProcessInfo
  -> IO ()
announceToAgent context (host, port) processInfo = do
  fileDescriptor <-
    STM.atomically $ STM.readTVar $ InternalContext.fileDescriptor context
  let
    manager = InternalContext.httpManager context
    discoveryUrl =
      URL.mkHttp host port haskellDiscoveryPath
    nonContainerPid = ProcessInfo.pidString processInfo
    (pidStr, pidFromParentNS) =
       case ProcessInfo.parentNsPid processInfo of
         Just parentPid ->
           (parentPid, parentPid == nonContainerPid)
         Nothing ->
           (nonContainerPid, False)
    maybeFileDescriptorString = show <$> fileDescriptor
    maybeInodeLinkPath =
      (\fdStr -> "/proc/" ++ nonContainerPid ++ "/fd/" ++ fdStr) <$>
        maybeFileDescriptorString
  discoveryRequestBase <- HTTP.parseUrlThrow $ show discoveryUrl
  inode <-
    case maybeInodeLinkPath of
      Just inodeLinkPath ->
        catch
          (do
            i <- PosixFiles.readSymbolicLink inodeLinkPath
            return $ Just i
          )
          (\e -> do
            debugM instanaLogger $
              "Could not obtain inode for process matching from " ++
              inodeLinkPath ++ ": " ++ show (e :: SomeException)
            return Nothing
          )
      Nothing ->
        return Nothing

  let
    haskellProcess =
      Aeson.object
        [ "pid"             .= pidStr
        , "pidFromParentNS" .= pidFromParentNS
        , "progName"        .= ProcessInfo.programName processInfo
        , "execPath"        .= ProcessInfo.executablePath processInfo
        , "args"            .= ProcessInfo.arguments processInfo
        , "fd"              .= maybeFileDescriptorString
        , "inode"           .= inode
        ]

    announceRequest = discoveryRequestBase
       { HTTP.method = "PUT"
       , HTTP.requestBody = HTTP.RequestBodyLBS $ Aeson.encode haskellProcess
       , HTTP.requestHeaders =
         [ ("Accept", "application/json")
         , ("Content-Type", "application/json; charset=UTF-8'")
         ]
       }
    announceRequestAction = HTTP.httpLbs announceRequest manager

  maybeAnnounceResponse <-
    catch
      (Retry.retryRequest
         Retry.announceRetryPolicy
         decodeAnnounceResponse
         announceRequestAction
      )
      (\e -> do
        warningM instanaLogger $ show (e :: SomeException)
        return Nothing
      )

  -- maybeAnnounceResponse is guaranteed to be Just _ if request succeeded and
  -- response was parsed succesfully, it is only Nothing when an exception
  -- has been catched, which should actually never happen, because we retry
  -- indefinitely.
  if isJust maybeAnnounceResponse
  then do
    let
      Just announceResponse = maybeAnnounceResponse
    STM.atomically $
      STM.writeTVar (InternalContext.connectionState context) $
        Announced (host, port)
    debugM instanaLogger $
      "Haskell process " ++ pidStr ++
      " has been successfully announced to agent at " ++ show (host, port) ++
      ", now waiting for the agent to be ready to accept data."

    -- transition to next phase of sensor-agent handshake
    AgentReady.waitUntilAgentIsReadyToAcceptData
      context
      pidStr
      processInfo
      announceResponse
  else do
    warningM instanaLogger $
      "Could not establish agent connection for process " ++ pidStr ++
      " (announce failed), will retry later."
    STM.atomically $ STM.writeTVar
      (InternalContext.connectionState context)
      Unconnected


{-| Decodes the JSON response returned by the announce request.
-}
decodeAnnounceResponse ::
  HTTP.Response ByteString
  -> IO (Maybe AnnounceResponse)
decodeAnnounceResponse response = do
  let
    body = HTTP.responseBody response
    maybeParsed :: Maybe AnnounceResponse
    maybeParsed = Aeson.decode body
  case maybeParsed of
    Just _ -> do
      return maybeParsed
    Nothing ->
      fail $ "Can't parse announce response" ++ (show body)