{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}

-- This is an approximate implementation of https://www.w3.org/TR/trace-context

module OpenTelemetry.Propagation where

import Control.Applicative
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import Data.Char (ord)
import Data.List (find)
import Data.String
import Data.Word
import OpenTelemetry.SpanContext
import Text.Printf

data PropagationFormat = PropagationFormat
  { propagateFromHeaders :: forall key. (Semigroup key, IsString key, Eq key) => [(key, BS.ByteString)] -> Maybe SpanContext,
    propagateToHeaders :: forall key. (Semigroup key, IsString key, Eq key) => SpanContext -> [(key, BS.ByteString)]
  }

-- | (p1 <> p2) parses like p1, then p2 as a fallback. (p1 <> p2) injects like p1.
instance Semigroup PropagationFormat where
  PropagationFormat from1 to1 <> PropagationFormat from2 _to2 =
    let from headers = from1 headers <|> from2 headers
        to context = to1 context
     in PropagationFormat from to

w3cTraceContext :: PropagationFormat
w3cTraceContext = PropagationFormat from to
  where
    from headers =
      case find ((== "traceparent") . fst) headers of
        Just (_, (parseSpanContext -> mctx)) -> mctx
        _ -> Nothing
    to (SpanContext (SId sid) (TId tid)) =
      [("traceparent", BS8.pack $ printf "00-%x-%x-00" tid sid)]

b3 :: PropagationFormat
b3 = typical_opentracing_format_with_prefix "x-b3-"

otTracer :: PropagationFormat
otTracer = typical_opentracing_format_with_prefix "ot-tracer-"

typical_opentracing_format_with_prefix :: String -> PropagationFormat
typical_opentracing_format_with_prefix prefix = PropagationFormat from to
  where
    to (SpanContext (SId sid) (TId tid)) =
      [ (fromString prefix <> "traceid", encode_u64 tid),
        (fromString prefix <> "spanid", encode_u64 sid),
        (fromString prefix <> "sampled", "true")
      ]
    from headers =
      let traceidKey = fromString prefix <> "traceid"
          spanidKey = fromString prefix <> "spanid"
          go _ (Just tid, Just sid) = Just (tid, sid)
          go [] _ = Nothing
          go ((k, v) : xs) (tid, sid)
            | k == traceidKey = go xs (decode_u64 v, sid)
            | k == spanidKey = go xs (tid, decode_u64 v)
            | otherwise = go xs (tid, sid)
       in (\(t, s) -> SpanContext (SId s) (TId t)) <$> go headers (Nothing, Nothing)

parseSpanContext :: BS.ByteString -> Maybe SpanContext
parseSpanContext input =
  case BS8.split '-' input of
    ["00", (fromHex -> Just tid), (fromHex -> Just sid), _] ->
      Just $ SpanContext (SId sid) (TId tid)
    _ -> Nothing

isLowerHexDigit :: Char -> Bool
isLowerHexDigit (ord -> w) = (w >= 48 && w <= 57) || (w >= 97 && w <= 102)

fromHex :: BS.ByteString -> Maybe Word64
fromHex bytes = BS8.foldl' go (Just 0) bytes
  where
    go Nothing _ = Nothing
    go (Just !result) (ord -> d) | d >= 48 && d < 58 = Just $ result * 16 + fromIntegral d - 48
    go (Just result) (ord -> d) | d >= 97 && d < 124 = Just $ result * 16 + fromIntegral d - 87
    go _ _ = Nothing

encode_u64 :: Word64 -> BS.ByteString
encode_u64 x = BS8.pack (printf "%016x" x)

decode_u64 :: BS.ByteString -> Maybe Word64
decode_u64 bytes | BS.length bytes > 16 = Nothing
decode_u64 bytes = BS.foldl' go (Just 0) bytes
  where
    go Nothing _ = Nothing
    go (Just !result) d | d >= 48 && d < 58 = Just $ result * 16 + fromIntegral d - 48
    go (Just result) d | d >= 97 && d < 124 = Just $ result * 16 + fromIntegral d - 87
    go _ _ = Nothing
