%%%------------------------------------------------------------------------
%% Copyright 2019, OpenTelemetry Authors
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% @doc
%% Span behaviour.
%% @end
%%%-------------------------------------------------------------------------
-module(otel_span).

-export([trace_id/1,
         span_id/1,
         hex_span_ctx/1,
         hex_trace_id/1,
         hex_span_id/1,
         tracestate/1,
         is_recording/1,
         is_valid/1,
         is_valid_name/1,
         validate_start_opts/1,
         set_attribute/3,
         set_attributes/2,
         add_event/3,
         add_events/2,
         record_exception/5,
         record_exception/6,
         set_status/2,
         set_status/3,
         update_name/2,
         end_span/1,
         end_span/2]).

-include("opentelemetry.hrl").

-define(is_recording(SpanCtx), SpanCtx =/= undefined andalso SpanCtx#span_ctx.is_recording =:= true).

-type start_opts() :: #{attributes => opentelemetry:attributes_map(),
                        links => [opentelemetry:link()],
                        is_recording => boolean(),
                        start_time => opentelemetry:timestamp(),
                        kind => opentelemetry:span_kind()}.
-type start_config() :: #{attributes := opentelemetry:attributes_map(),
                            links := [opentelemetry:link()],
                            is_recording := boolean(),
                            start_time := opentelemetry:timestamp(),
                            kind := opentelemetry:span_kind()}.
%% Start options for a span.

-export_type([start_opts/0, start_config/0]).

%% @doc Validates the start options for a span and fills in defaults.
-spec validate_start_opts(start_opts()) -> start_config().
validate_start_opts(Opts) when is_map(Opts) ->
    Attributes = maps:get(attributes, Opts, #{}),
    Links = maps:get(links, Opts, []),
    Kind = maps:get(kind, Opts, ?SPAN_KIND_INTERNAL),
    StartTime = maps:get(start_time, Opts, opentelemetry:timestamp()),
    IsRecording = maps:get(is_recording, Opts, true),
    #{
      attributes => otel_attributes:process_attributes(Attributes),
      links => Links,
      kind => Kind,
      start_time => StartTime,
      is_recording => IsRecording
     }.

%% @doc Returns whether the span is recording.
-spec is_recording(SpanCtx) -> boolean() when
      SpanCtx :: opentelemetry:span_ctx() | undefined.
is_recording(SpanCtx) ->
    ?is_recording(SpanCtx).

%% @doc Returns whether the span context is valid.
-spec is_valid(SpanCtx) -> boolean() when
      SpanCtx :: opentelemetry:span_ctx().
is_valid(#span_ctx{trace_id=TraceId,
                   span_id=SpanId}) when TraceId =/= 0 ,
                                         SpanId =/= 0 ->
    true;
is_valid(_) ->
    false.

%% @private
-spec is_valid_name(any()) -> boolean().
is_valid_name(undefined) ->
    false;
is_valid_name(Name) when is_atom(Name) orelse (is_binary(Name) andalso Name =/= <<"">>) ->
    true;
is_valid_name(_) ->
    false.

%% accessors

%% @doc Returns the trace ID of the given span context.
-spec trace_id(opentelemetry:span_ctx()) -> opentelemetry:trace_id().
trace_id(#span_ctx{trace_id=TraceId}) ->
    TraceId.

%% @doc Returns the span ID of the given span context.
-spec span_id(opentelemetry:span_ctx()) -> opentelemetry:span_id().
span_id(#span_ctx{span_id=SpanId}) ->
    SpanId.

%% keys are prefixed with `otel_' because the main use of this function is logger metadata
-spec hex_span_ctx(opentelemetry:span_ctx() | undefined) -> #{otel_trace_id := opentelemetry:hex_trace_id(),
                                                              otel_span_id := opentelemetry:hex_span_id(),
                                                              otel_trace_flags := binary()} | #{}.
hex_span_ctx(SpanCtx = #span_ctx{trace_flags=TraceFlags}) ->
    TraceIdBin = hex_trace_id(SpanCtx),
    SpanIdBin = hex_span_id(SpanCtx),
    TraceFlagsBin = case TraceFlags band 1 of 1 -> <<"01">>; _ -> <<"00">> end,
    #{otel_trace_id => TraceIdBin,
      otel_span_id => SpanIdBin,
      otel_trace_flags => TraceFlagsBin};
hex_span_ctx(_) ->
    #{}.

-spec hex_trace_id(opentelemetry:span_ctx()) -> opentelemetry:hex_trace_id().
hex_trace_id(#span_ctx{hex_trace_id=HexTraceId}) ->
    HexTraceId.

-spec hex_span_id(opentelemetry:span_ctx()) -> opentelemetry:hex_span_id().
hex_span_id(#span_ctx{hex_span_id=HexSpanId}) ->
    HexSpanId.

-spec tracestate(opentelemetry:span_ctx() | undefined) -> otel_tracestate:t().
tracestate(#span_ctx{tracestate=Tracestate}) ->
    Tracestate;
tracestate(_) ->
    otel_tracestate:new().

-spec set_attribute(SpanCtx, Key, Value) -> boolean() when
      Key :: opentelemetry:attribute_key(),
      Value :: opentelemetry:attribute_value(),
      SpanCtx :: opentelemetry:span_ctx().
set_attribute(SpanCtx=#span_ctx{span_sdk={Module, _}}, Key, Value) when ?is_recording(SpanCtx) , is_tuple(Value) ->
    List = tuple_to_list(Value),
    case otel_attributes:is_valid_attribute(Key, List) of
        true ->
            Module:set_attribute(SpanCtx, Key, List);
        false ->
            false
    end;
set_attribute(SpanCtx=#span_ctx{span_sdk={Module, _}}, Key, Value) when ?is_recording(SpanCtx) ->
    case otel_attributes:is_valid_attribute(Key, Value) of
        true ->
            Module:set_attribute(SpanCtx, Key, Value);
        false ->
            false
    end;
set_attribute(_, _, _) ->
    false.

-spec set_attributes(SpanCtx, Attributes) -> boolean() when
      Attributes :: opentelemetry:attributes_map(),
      SpanCtx :: opentelemetry:span_ctx().
set_attributes(SpanCtx=#span_ctx{span_sdk={Module, _}}, Attributes) when ?is_recording(SpanCtx),
                                                                         (is_list(Attributes) orelse is_map(Attributes)) ->
    Module:set_attributes(SpanCtx, otel_attributes:process_attributes(Attributes));
set_attributes(_, _) ->
    false.

%% @doc Adds an event to the given span context.
%%
%% Returns `false' if the given span context is not recording, or if the event `Name' is
%% not valid.
-spec add_event(SpanCtx, Name, Attributes) -> boolean() when
      Name :: opentelemetry:event_name(),
      Attributes :: opentelemetry:attributes_map(),
      SpanCtx :: opentelemetry:span_ctx().
add_event(SpanCtx=#span_ctx{span_sdk={Module, _}}, Name, Attributes)
  when ?is_recording(SpanCtx) ,
       (is_list(Attributes) orelse is_map(Attributes)) ->
    case is_valid_name(Name) of
        true ->
            Module:add_event(SpanCtx, Name, otel_attributes:process_attributes(Attributes));
        false ->
            false
    end;
add_event(_, _, _) ->
    false.

%% @doc Same as {@link add_event/3}, but takes a list of events.
%%
%% Returns `false' if the given span context is not recording.
-spec add_events(SpanCtx, Events) -> boolean() when
      Events :: [opentelemetry:event()],
      SpanCtx :: opentelemetry:span_ctx().
add_events(SpanCtx=#span_ctx{span_sdk={Module, _}}, Events) when ?is_recording(SpanCtx) , is_list(Events)  ->
    Module:add_events(SpanCtx, Events);
add_events(_, _) ->
    false.

-spec record_exception(SpanCtx, Class, Term, Stacktrace, Attributes) -> boolean() when
      SpanCtx :: opentelemetry:span_ctx(),
      Class :: atom(),
      Term :: term(),
      Stacktrace :: list(any()),
      Attributes :: opentelemetry:attributes_map().
record_exception(SpanCtx, Class, Term, Stacktrace, Attributes) when is_list(Attributes) ->
    record_exception(SpanCtx, Class, Term, Stacktrace, maps:from_list(Attributes));
record_exception(SpanCtx, Class, Term, Stacktrace, Attributes) when is_map(Attributes) ->
    {ok, ExceptionType} = otel_utils:format_binary_string("~0tP:~0tP", [Class, 10, Term, 10], [{chars_limit, 50}]),
    {ok, StacktraceString} = otel_utils:format_binary_string("~0tP", [Stacktrace, 10], [{chars_limit, 50}]),
    ExceptionAttributes = #{'exception.type' => ExceptionType,
                            'exception.stacktrace' => StacktraceString},
    add_event(SpanCtx, 'exception', maps:merge(ExceptionAttributes, Attributes));
record_exception(_, _, _, _, _) ->
    false.

-spec record_exception(SpanCtx, Class, Term,  Message, Stacktrace, Attributes) -> boolean() when
      SpanCtx :: opentelemetry:span_ctx(),
      Class :: atom(),
      Term :: term(),
      Message :: unicode:unicode_binary(),
      Stacktrace :: list(any()),
      Attributes :: opentelemetry:attributes_map().
record_exception(SpanCtx, Class, Term, Message, Stacktrace, Attributes) when is_list(Attributes) ->
    record_exception(SpanCtx, Class, Term, Message, Stacktrace, maps:from_list(Attributes));
record_exception(SpanCtx, Class, Term, Message, Stacktrace, Attributes) when is_map(Attributes) ->
    {ok, ExceptionType} = otel_utils:format_binary_string("~0tP:~0tP", [Class, 10, Term, 10], [{chars_limit, 50}]),
    {ok, StacktraceString} = otel_utils:format_binary_string("~0tP", [Stacktrace, 10], [{chars_limit, 50}]),
    ExceptionAttributes = #{'exception.type' => ExceptionType,
                            'exception.stacktrace' => StacktraceString,
                            'exception.message' => Message},
    add_event(SpanCtx, 'exception', maps:merge(ExceptionAttributes, Attributes));
record_exception(_, _, _, _, _, _) ->
    false.

-spec set_status(SpanCtx, StatusOrCode) -> boolean() when
      StatusOrCode :: opentelemetry:status() | undefined | opentelemetry:status_code(),
      SpanCtx :: opentelemetry:span_ctx().
set_status(SpanCtx=#span_ctx{span_sdk={Module, _}}, Code) when ?is_recording(SpanCtx) andalso
                                                               (Code =:= ?OTEL_STATUS_UNSET orelse
                                                                Code =:= ?OTEL_STATUS_OK orelse
                                                                Code =:= ?OTEL_STATUS_ERROR)->
    Module:set_status(SpanCtx, opentelemetry:status(Code));
set_status(SpanCtx=#span_ctx{span_sdk={Module, _}}, undefined) when ?is_recording(SpanCtx) ->
    Module:set_status(SpanCtx, opentelemetry:status(?OTEL_STATUS_UNSET));
set_status(SpanCtx=#span_ctx{span_sdk={Module, _}}, Status) when ?is_recording(SpanCtx) ->
    Module:set_status(SpanCtx, Status);
set_status(_, _) ->
    false.

-spec set_status(SpanCtx, Code, Message) -> boolean() when
      Code :: opentelemetry:status_code(),
      Message :: unicode:unicode_binary(),
      SpanCtx :: opentelemetry:span_ctx().
set_status(SpanCtx, Code, Message) ->
    set_status(SpanCtx, opentelemetry:status(Code, Message)).

%% @doc Updates the name of the given span context to `Name'.
%%
%% Returns `false' if the given span context is not recording, or if the name `Name' is not valid.
-spec update_name(SpanCtx, Name) -> boolean() when
      Name :: opentelemetry:span_name(),
      SpanCtx :: opentelemetry:span_ctx().
update_name(SpanCtx=#span_ctx{span_sdk={Module, _}}, SpanName) when ?is_recording(SpanCtx) ->
    case is_valid_name(SpanName) of
        true ->
            Module:update_name(SpanCtx, SpanName);
        false ->
            false
    end;
update_name(_, _) ->
    false.

%% @doc Ends the given span context.
%%
%% If `SpanCtx' is not recording, this function doesn't do anything.
%% Returns the updated span context.
-spec end_span(SpanCtx) -> SpanCtx when
      SpanCtx :: opentelemetry:span_ctx().
end_span(SpanCtx=#span_ctx{span_sdk={Module, _}}) when ?is_recording(SpanCtx) ->
    _ = Module:end_span(SpanCtx, undefined),
    SpanCtx#span_ctx{is_recording=false};
end_span(SpanCtx) ->
    SpanCtx.

%% @doc Ends the given span context with the given timestamp.
%%
%% If `SpanCtx' is not recording, this function doesn't do anything.
%% If `Timestamp' is `undefined', this is equivalent to {@link end_span/1}.
%% Returns the updated span context.
-spec end_span(SpanCtx, Timestamp) -> SpanCtx when
    SpanCtx :: opentelemetry:span_ctx(),
    Timestamp :: integer() | undefined.
end_span(SpanCtx=#span_ctx{span_sdk={Module, _}}, Timestamp) when ?is_recording(SpanCtx)
                                                                  , is_integer(Timestamp) ->
    _ = Module:end_span(SpanCtx, Timestamp),
    SpanCtx#span_ctx{is_recording=false};
end_span(SpanCtx=#span_ctx{span_sdk={Module, _}}, _Timestamp) when ?is_recording(SpanCtx) ->
    _ = Module:end_span(SpanCtx),
    SpanCtx#span_ctx{is_recording=false};
end_span(SpanCtx, _) ->
    SpanCtx.
