-- SPDX-FileCopyrightText: 2026 Alexandra de Wit
--
-- SPDX-License-Identifier: MIT

{- | The span read and the @dd@ stamping behind "Ecluse.Runtime.Telemetry.Correlation", which
documents the correlation and re-exports the curated surface. Importing this module opts out of
that stability promise, the convention @text@ and @bytestring@ use, so production code imports
the public one.
-}
module Ecluse.Runtime.Telemetry.Correlation.Internal (
    -- * Identity
    ddIdentity,
    ddIdentityFromEnvironment,

    -- * Active-span correlation
    activeDdSpan,
    ddContextNow,
    ddPayloadNow,
) where

import System.Environment (getEnvironment)

import Katip (SimpleLogPayload)
import OpenTelemetry.Propagator.Datadog (
    convertOpenTelemetrySpanIdToDatadogSpanId,
    convertOpenTelemetryTraceIdToDatadogTraceId,
 )
import OpenTelemetry.Trace.Core (getActiveSpanContext, isValid)
import OpenTelemetry.Trace.Core qualified as OTel

import Ecluse.Runtime.Log (
    DdContext (..),
    DdSpan (DdSpan),
    ddField,
 )
import Ecluse.Runtime.Telemetry.Resolve (
    ResolvedTelemetry (rtEnvironment, rtServiceName, rtVersion),
    resolveTelemetry,
 )

{- | The span-less @dd@ identity that stamps every log line. 'ddPayloadNow' fills the span at
log time. The one resolved configuration feeds the SDK too, so logs and traces share an identity.
-}
ddIdentity :: ResolvedTelemetry -> DdContext
ddIdentity :: ResolvedTelemetry -> DdContext
ddIdentity ResolvedTelemetry
resolved =
    DdContext
        { ddService :: Text
ddService = ResolvedTelemetry -> Text
rtServiceName ResolvedTelemetry
resolved
        , ddEnv :: Maybe Text
ddEnv = ResolvedTelemetry -> Maybe Text
rtEnvironment ResolvedTelemetry
resolved
        , ddVersion :: Maybe Text
ddVersion = ResolvedTelemetry -> Maybe Text
rtVersion ResolvedTelemetry
resolved
        , ddSpan :: Maybe DdSpan
ddSpan = Maybe DdSpan
forall a. Maybe a
Nothing
        }

{- | Resolve the @dd@ identity from the environment on "Ecluse.Runtime.Telemetry.Resolve"'s
precedence, so the log identity matches the exporter's. Call once at composition, never per line.
-}
ddIdentityFromEnvironment :: IO DdContext
ddIdentityFromEnvironment :: IO DdContext
ddIdentityFromEnvironment = ResolvedTelemetry -> DdContext
ddIdentity (ResolvedTelemetry -> DdContext)
-> ([(String, String)] -> ResolvedTelemetry)
-> [(String, String)]
-> DdContext
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [(String, String)] -> ResolvedTelemetry
resolveTelemetry ([(String, String)] -> DdContext)
-> IO [(String, String)] -> IO DdContext
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IO [(String, String)]
getEnvironment

{- | The active span's ids in Datadog format, 'Nothing' when no span is in scope or its context
is not valid, so a log line never carries an all-zero trace id.
-}
activeDdSpan :: (MonadIO m) => m (Maybe DdSpan)
activeDdSpan :: forall (m :: * -> *). MonadIO m => m (Maybe DdSpan)
activeDdSpan = do
    mContext <- m (Maybe SpanContext)
forall (m :: * -> *). MonadIO m => m (Maybe SpanContext)
getActiveSpanContext
    pure $ case mContext of
        Just SpanContext
spanContext
            | SpanContext -> Bool
isValid SpanContext
spanContext ->
                DdSpan -> Maybe DdSpan
forall a. a -> Maybe a
Just
                    ( Text -> Text -> DdSpan
DdSpan
                        (Word64 -> Text
forall b a. (Show a, IsString b) => a -> b
show (TraceId -> Word64
convertOpenTelemetryTraceIdToDatadogTraceId (SpanContext -> TraceId
OTel.traceId SpanContext
spanContext)))
                        (Word64 -> Text
forall b a. (Show a, IsString b) => a -> b
show (SpanId -> Word64
convertOpenTelemetrySpanIdToDatadogSpanId (SpanContext -> SpanId
OTel.spanId SpanContext
spanContext)))
                    )
        Maybe SpanContext
_ -> Maybe DdSpan
forall a. Maybe a
Nothing

{- | Fill the active span's ids onto a @dd@ identity, yielding the 'DdContext' for the current
log site.
-}
ddContextNow :: (MonadIO m) => DdContext -> m DdContext
ddContextNow :: forall (m :: * -> *). MonadIO m => DdContext -> m DdContext
ddContextNow DdContext
base = do
    mSpan <- m (Maybe DdSpan)
forall (m :: * -> *). MonadIO m => m (Maybe DdSpan)
activeDdSpan
    pure base{ddSpan = mSpan}

{- | The @dd@ object for the current log site as a @katip@ payload. Install it as a request or
worker scope's initial context so every line under that scope carries @dd@.
-}
ddPayloadNow :: (MonadIO m) => DdContext -> m SimpleLogPayload
ddPayloadNow :: forall (m :: * -> *). MonadIO m => DdContext -> m SimpleLogPayload
ddPayloadNow DdContext
base = DdContext -> SimpleLogPayload
ddField (DdContext -> SimpleLogPayload)
-> m DdContext -> m SimpleLogPayload
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> DdContext -> m DdContext
forall (m :: * -> *). MonadIO m => DdContext -> m DdContext
ddContextNow DdContext
base