{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE TypeApplications      #-}
{-# LANGUAGE TypeOperators         #-}
{-# LANGUAGE ViewPatterns          #-}

-- | An HTTP Client to post data to a datadog agent.
--
-- Many of our refinements are stricter than the actual requirements, to err on
-- the side of caution, and because the actual requirements are very complex.
module Datadog.Client where

import           Control.Monad             (unless, void)
import           Data.Char                 (isAlpha, isAsciiLower, isDigit)
import           Data.Int                  (Int64)
import qualified Data.List.NonEmpty        as NEL
import           Data.Map.Strict           (Map)
import qualified Data.Map.Strict           as M
import           Data.Text                 (Text)
import qualified Data.Text                 as T
import           Data.Text.Arbitrary       ()
import           Data.Text.Prettyprint.Doc (viaShow)
import           Data.Time                 (NominalDiffTime, UTCTime)
import           Data.Time.Clock.POSIX     (utcTimeToPOSIXSeconds)
import           Data.Typeable             (Proxy (..), Typeable, typeOf)
import           Data.Word                 (Word64)
import           Refined
import           Servant.Client            (ClientM, client)

import qualified Datadog.Agent             as API

type DDText = NonEmpty && (SizeLessThan 101)

newtype SpanId = SpanId (Refined NonZero Word64)
newtype TraceId = TraceId (Refined NonZero Word64)
newtype ServiceName = ServiceName (Refined (DDText && Tag) Text)

data Trace = Trace
  { tService :: ServiceName
  , tId      :: TraceId
  , tSpans   :: (Refined NonEmpty (Map SpanId Span))
  }

newtype SpanName = SpanName (Refined (DDText && HasAlpha) Text)
newtype MetaKey = MetaKey (Refined (DDText && Tag) Text) deriving (Eq, Ord)
newtype MetaValue = MetaValue (Refined DDText Text)

data Span = Span
  { sName     :: SpanName
  , sParentId :: Maybe SpanId
  , sStart    :: UTCTime
  , sDuration :: NominalDiffTime
  , sMeta     :: Maybe (Map MetaKey MetaValue)
  }

traces :: NEL.NonEmpty Trace -> ClientM ()
traces (NEL.toList -> ts) = void . raw $ toAPI <$> ts
  where
    raw = client (Proxy @ API.Traces3)

    toAPI :: Trace -> API.Trace
    toAPI (trace@(Trace _ _ (M.toList . unrefine -> spans))) =
      API.Trace $ (mkSpan trace) <$> spans

    mkSpan :: Trace -> (SpanId, Span) -> API.Span
    mkSpan (Trace (ServiceName (unrefine -> serviceName))
                  (TraceId (unrefine -> traceId))
                  _)
           ((SpanId (unrefine -> spanId)),
            (Span (SpanName (unrefine -> spanName))
             parent
             start
             duration
             meta)) =
      API.Span serviceName
               spanName
               "time" -- not using resource, but it is required
               traceId
               spanId
               ((\(SpanId (unrefine -> p)) -> p) <$> parent)
               (timeToNanos start)
               (nominalToNanos duration)
               Nothing -- not using error
               ((\m -> (M.map unValue) . (M.mapKeys unKey) $ m) <$> meta)
               Nothing -- not using metrics
               Nothing -- not using type

    unKey (MetaKey (unrefine -> k)) = k
    unValue (MetaValue (unrefine -> v)) = v

    timeToNanos :: UTCTime -> Int64
    timeToNanos time = nominalToNanos $ utcTimeToPOSIXSeconds time

    nominalToNanos :: NominalDiffTime -> Int64
    nominalToNanos time =
      let (nanos, _) = properFraction (1000000000 * time)
      in  nanos

data Tag
instance Predicate Tag Text where
   validate p txt = validate' p (T.all isValidChar) txt
                    where
                      -- dumbed down `normalizeTag`
                      isValidChar c = case c of
                        ':' -> True
                        '.' -> True
                        '/' -> True
                        '-' -> True
                        c'  -> isAsciiLower c' || isDigit c'

data HasAlpha
instance Predicate HasAlpha Text where
   validate p txt = validate' p (T.any isAlpha) txt

validate' :: (Typeable t, Monad m, Show a) => t -> (a -> Bool) -> a -> RefineT m ()
validate' t p a =
  unless (p a) $
  throwRefineOtherException (typeOf t) ("failed predicate: " <> (viaShow a))