{-# LANGUAGE OverloadedStrings #-}

module Control.Monad.Trace.Internal (
  TraceID(..), encodeTraceID, decodeTraceID, randomTraceID,
  SpanID(..), encodeSpanID, decodeSpanID, randomSpanID,
  Context(..),
  Name,
  Span(..),
  SamplingDecision(..), spanIsSampled, spanIsDebug,
  Reference(..),
  Key, Value(..)
) where

import Control.Monad (replicateM)
import qualified Data.Aeson as JSON
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS.Char8
import qualified Data.ByteString.Base16 as Base16
import Data.Map.Strict (Map)
import Data.Set (Set)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock.POSIX (POSIXTime)
import System.Random (randomIO)

-- | The name of a span.
type Name = Text

-- | The type of annotations' keys.
--
-- Keys starting with double underscores are reserved and should not be used.
type Key = Text

-- | A 128-bit trace identifier.
newtype TraceID = TraceID ByteString deriving (Eq, Ord, Show)

-- | Hex-encodes a trace ID.
encodeTraceID :: TraceID -> Text
encodeTraceID (TraceID bs) = hexEncode bs

-- | Decodes a traced ID from a hex-encoded string.
decodeTraceID :: Text -> Maybe TraceID
decodeTraceID txt = case hexDecode txt of
  Just bs | BS.length bs == 16 -> Just $ TraceID bs
  _ -> Nothing

instance JSON.FromJSON TraceID where
  parseJSON = JSON.withText "TraceID" $ maybe (fail "invalid hex-encoding") pure . decodeTraceID

instance JSON.ToJSON TraceID where
  toJSON = JSON.toJSON . encodeTraceID

-- | Generates a random trace ID.
randomTraceID :: IO TraceID
randomTraceID = TraceID <$> randomID 16

-- | A 64-bit span identifier.
newtype SpanID = SpanID ByteString deriving (Eq, Ord, Show)

-- | Hex-encodes a span ID.
encodeSpanID :: SpanID -> Text
encodeSpanID (SpanID bs) = hexEncode bs

-- | Decodes a span ID from a hex-encoded string.
decodeSpanID :: Text -> Maybe SpanID
decodeSpanID txt = case hexDecode txt of
  Just bs | BS.length bs == 8 -> Just $ SpanID bs
  _ -> Nothing

instance JSON.FromJSON SpanID where
  parseJSON = JSON.withText "SpanID" $ maybe (fail "invalid hex-encoding") pure . decodeSpanID

instance JSON.ToJSON SpanID where
  toJSON = JSON.toJSON . encodeSpanID

-- | Generates a random span ID.
randomSpanID :: IO SpanID
randomSpanID = SpanID <$> randomID 8

-- | A fully qualified span identifier, containing both the ID of the trace the span belongs to and
-- the span's ID.
data Context = Context
  { contextTraceID :: !TraceID
  , contextSpanID :: !SpanID
  , contextBaggages :: !(Map Key ByteString)
  } deriving (Eq, Ord, Show)

-- | A relationship between spans.
--
-- There are currently two types of references, both of which model direct causal relationships
-- between a child and a parent. More background on references is available in the opentracing
-- specification: https://github.com/opentracing/specification/blob/master/specification.md.
data Reference
  = ChildOf !SpanID
  -- ^ 'ChildOf' references imply that the parent span depends on the child span in some capacity.
  -- Note that this reference type is only valid within a single trace.
  | FollowsFrom !Context
  -- ^ If the parent does not depend on the child, we use a 'FollowsFrom' reference.
  deriving (Eq, Ord, Show)

-- | Metadata attached to a span.
data Value
  = TagValue !JSON.Value
  | LogValue !JSON.Value !(Maybe POSIXTime)

-- | A part of a trace.
data Span = Span
  { spanName :: !Name
  , spanContext :: !Context
  , spanReferences :: !(Set Reference)
  , spanSamplingDecision :: !SamplingDecision
  }

-- | A span's sampling decision.
data SamplingDecision
  = Always
  | Never
  | Debug
  deriving (Eq, Ord, Enum, Show)

-- | Returns whether the span is sampled.
spanIsSampled :: Span -> Bool
spanIsSampled spn = spanSamplingDecision spn /= Never

-- | Returns whether the span has debug enabled.
spanIsDebug :: Span -> Bool
spanIsDebug spn = spanSamplingDecision spn == Debug

randomID :: Int -> IO ByteString
randomID len = BS.pack <$> replicateM len randomIO

hexDecode :: Text-> Maybe ByteString
hexDecode t = case Base16.decode $ BS.Char8.pack $ T.unpack t of
  (bs, trail) | BS.null trail -> Just bs
  _ -> Nothing

hexEncode :: ByteString -> Text
hexEncode = T.pack . BS.Char8.unpack . Base16.encode