-module(otel_telemetry).

-include_lib("kernel/include/logger.hrl").
-include_lib("opentelemetry_api/include/opentelemetry.hrl").

-export([
         init/1,
         init/2,
         handle_event/4,
         start_telemetry_span/4,
         set_current_telemetry_span/2,
         end_telemetry_span/2
]).

-type telemetry_span_ctx() :: opentelemetry:span_ctx().
-type parent_span_ctx() :: opentelemetry:span_ctx().

-type ctx_set() :: {parent_span_ctx(), telemetry_span_ctx()}.

-spec init(atom()) -> ok.
init(Application) ->
    init(Application, []).

-spec init(atom(), []) -> ok.
init(_Application, _Opts) ->
    ok.

-spec start_telemetry_span(atom(), opentelemetry:span_name(), telemetry:event_metadata(), otel_span:start_opts()) -> opentelemetry:span_ctx().
start_telemetry_span(TracerId, SpanName, EventMetadata, Opts) ->
    ParentCtx = otel_tracer:current_span_ctx(),
    Tracer = opentelemetry:get_application_tracer(TracerId),
    Ctx = otel_tracer:start_span(Tracer, SpanName, Opts),
    otel_tracer:set_current_span(Ctx),
    _ = store_ctx({ParentCtx, Ctx}, TracerId, EventMetadata),
    Ctx.

-spec set_current_telemetry_span(atom(), telemetry:event_metadata()) -> opentelemetry:span_ctx() | undefined.
set_current_telemetry_span(TracerId, EventMetadata) ->
    case fetch_telemetry_span_ctx(TracerId, EventMetadata) of
        {_ParentCtx, Ctx} ->
            otel_tracer:set_current_span(Ctx),
            Ctx;
        undefined ->
            undefined
    end.

-spec end_telemetry_span(atom(), telemetry:event_metadata()) -> ok.
end_telemetry_span(TracerId, EventMetadata) ->
    Ctx = pop_ctx(TracerId, EventMetadata),
    case Ctx of
        {ParentCtx, SpanCtx} ->
            otel_span:end_span(SpanCtx),
            otel_tracer:set_current_span(ParentCtx),
            ok;
        undefined ->
            ok
    end.

-spec store_ctx(ctx_set(), atom(), telemetry:event_metadata()) -> ok.
store_ctx(SpanCtxSet, TracerId, EventMetadata) ->
    case maps:get(telemetry_span_context, EventMetadata, undefined) of
        undefined ->
            push_to_tracer_stack(SpanCtxSet, TracerId);
        TelemetryCtx ->
            erlang:put({otel_telemetry, TelemetryCtx}, SpanCtxSet)
    end,
    ok.

-spec push_to_tracer_stack(ctx_set(), atom()) -> ok.
push_to_tracer_stack(SpanCtxSet, TracerId) ->
    case erlang:get({otel_telemetry, TracerId}) of
        undefined ->
            erlang:put({otel_telemetry, TracerId}, [SpanCtxSet]);
        Stack ->
            erlang:put({otel_telemetry, TracerId}, [SpanCtxSet | Stack])
    end.

-spec fetch_telemetry_span_ctx(atom(), telemetry:event_metadata()) -> ctx_set() | undefined.
fetch_telemetry_span_ctx(TracerId, EventMetadata) ->
    case maps:get(telemetry_span_context, EventMetadata, undefined) of
        undefined ->
            peek_from_tracer_stack(TracerId);
        TelemetryCtx ->
            erlang:get({otel_telemetry, TelemetryCtx})
    end.

-spec peek_from_tracer_stack(atom()) -> ctx_set() | undefined.
peek_from_tracer_stack(TracerId) ->
    case erlang:get({otel_telemetry, TracerId}) of
        undefined ->
            undefined;
        [SpanCtxSet | _Rest] ->
            SpanCtxSet;
        [] ->
            ?LOG_DEBUG("`opentelemetry_telemetry` span ctx tracer stack for "
                       "TracerId ~p in Pid ~p is empty.", [TracerId, self()]),
            undefined
    end.

-spec pop_ctx(atom(), telemetry:event_metadata()) -> ctx_set().
pop_ctx(TracerId, EventMetadata) ->
    case maps:get(telemetry_span_context, EventMetadata, undefined) of
        undefined ->
            pop_from_tracer_stack(TracerId);
        TelemetryCtx ->
            erlang:erase({otel_telemetry, TelemetryCtx})
    end.

pop_from_tracer_stack(TracerId) ->
    case erlang:get({otel_telemetry, TracerId}) of
        undefined ->
            undefined;
        [SpanCtxSet | Rest] ->
            erlang:put({otel_telemetry, TracerId}, Rest),
            SpanCtxSet;
        [] ->
            ?LOG_DEBUG("`opentelemetry_telemetry` span ctx tracer stack for "
                       "TracerId ~p in Pid ~p is empty.", [TracerId, self()]),
            undefined
    end.

handle_event(_Event,
             _Measurements,
             Metadata,
             #{type := start, tracer_id := TracerId, span_name := Name}) ->
    _Ctx = start_telemetry_span(TracerId, Name, Metadata, #{}),
    ok;
handle_event(_Event,
             _Measurements,
             Metadata,
             #{type := stop, tracer_id := TracerId}) ->
    _Ctx = set_current_telemetry_span(TracerId, Metadata),
    end_telemetry_span(TracerId, Metadata),
    ok;
handle_event(_Event,
             _Measurements,
             #{kind := Kind, reason := Reason, stacktrace := Stacktrace} = Metadata,
             #{type := exception, tracer_id := TracerId}) ->
    Ctx = set_current_telemetry_span(TracerId, Metadata),
    Status = opentelemetry:status(?OTEL_STATUS_ERROR, atom_to_binary(Reason, utf8)),
    otel_span:record_exception(Ctx, Kind, Reason, Stacktrace, []),
    otel_span:set_status(Ctx, Status),
    end_telemetry_span(TracerId, Metadata),
    ok;
handle_event(_Event, _Measurements, _Metadata, _Config) ->
    ok.

