defmodule Raxol.Core.Telemetry.Context do
  @moduledoc """
  Trace context propagation for telemetry events.

  Provides request correlation across components using trace_id and span_id.
  Following OpenTelemetry-compatible patterns for distributed tracing.

  ## Design

  Trace context is stored in the process dictionary and automatically
  propagated to child processes. This allows correlating all telemetry
  events within a single request/operation.

  ## Usage

      # Start a new trace (e.g., at request entry point)
      Context.start_trace()

      # Start a child span within a trace
      Context.start_span(:render)

      # Get current context for telemetry metadata
      metadata = Context.to_metadata()
      :telemetry.execute([:raxol, :render, :stop], measurements, metadata)

      # End current span
      Context.end_span()

      # Wrap a function with span instrumentation
      Context.with_span(:database_query, fn ->
        # ... work ...
      end)

  ## Context Structure

      %{
        trace_id: "abc123def456",      # Unique per request/trace
        span_id: "span789",            # Unique per operation within trace
        parent_span_id: "span456",     # Parent span (for nesting)
        baggage: %{}                   # Additional context to propagate
      }
  """

  @trace_key :raxol_trace_context
  @span_stack_key :raxol_span_stack

  @type trace_id :: String.t()
  @type span_id :: String.t()
  @type t :: %{
          trace_id: trace_id(),
          span_id: span_id(),
          parent_span_id: span_id() | nil,
          baggage: map()
        }

  # ============================================================================
  # Trace Management
  # ============================================================================

  @doc """
  Starts a new trace with a unique trace_id.

  Call this at the entry point of a request or operation.
  Returns the trace context.

  ## Examples

      context = Context.start_trace()
      # => %{trace_id: "abc123...", span_id: "def456...", ...}

      # With custom trace_id (e.g., from incoming request header)
      context = Context.start_trace(trace_id: "incoming-trace-id")
  """
  @spec start_trace(keyword()) :: t()
  def start_trace(opts \\ []) do
    trace_id = Keyword.get(opts, :trace_id, generate_id())
    span_id = generate_id()

    context = %{
      trace_id: trace_id,
      span_id: span_id,
      parent_span_id: nil,
      baggage: Keyword.get(opts, :baggage, %{})
    }

    Process.put(@trace_key, context)
    Process.put(@span_stack_key, [])

    context
  end

  @doc """
  Gets the current trace context.

  Returns nil if no trace is active.
  """
  @spec get() :: t() | nil
  def get do
    Process.get(@trace_key)
  end

  @doc """
  Gets the current trace_id, or nil if no trace is active.
  """
  @spec trace_id() :: trace_id() | nil
  def trace_id do
    case get() do
      %{trace_id: id} -> id
      nil -> nil
    end
  end

  @doc """
  Gets the current span_id, or nil if no trace is active.
  """
  @spec span_id() :: span_id() | nil
  def span_id do
    case get() do
      %{span_id: id} -> id
      nil -> nil
    end
  end

  @doc """
  Clears the current trace context.

  Call this when a request/operation completes.
  """
  @spec clear() :: :ok
  def clear do
    Process.delete(@trace_key)
    Process.delete(@span_stack_key)
    :ok
  end

  # ============================================================================
  # Span Management
  # ============================================================================

  @doc """
  Starts a new span within the current trace.

  Spans can be nested - calling start_span while a span is active
  creates a child span.

  ## Examples

      Context.start_span(:render)
      # ... do rendering ...
      Context.end_span()
  """
  @spec start_span(atom()) :: t()
  def start_span(name) when is_atom(name) do
    case get() do
      nil ->
        # No active trace, start one
        start_trace()
        |> tap(fn _ -> start_span(name) end)

      context ->
        # Push current span onto stack
        stack = Process.get(@span_stack_key, [])
        Process.put(@span_stack_key, [{context.span_id, name} | stack])

        # Create new span
        new_span_id = generate_id()

        new_context = %{
          context
          | span_id: new_span_id,
            parent_span_id: context.span_id
        }

        Process.put(@trace_key, new_context)
        new_context
    end
  end

  @doc """
  Ends the current span and returns to the parent span.
  """
  @spec end_span() :: t() | nil
  def end_span do
    case get() do
      nil ->
        nil

      context ->
        stack = Process.get(@span_stack_key, [])

        case stack do
          [] ->
            # No parent span, just return current context
            context

          [{parent_span_id, _name} | rest] ->
            # Restore parent span
            Process.put(@span_stack_key, rest)

            parent_context = %{
              context
              | span_id: parent_span_id,
                parent_span_id: get_grandparent_span_id(rest)
            }

            Process.put(@trace_key, parent_context)
            parent_context
        end
    end
  end

  @doc """
  Executes a function within a new span.

  Automatically starts a span, executes the function, and ends the span.
  The span timing is captured for telemetry.

  ## Examples

      result = Context.with_span(:database_query, fn ->
        Repo.all(User)
      end)
  """
  @spec with_span(atom(), (-> result)) :: result when result: any()
  def with_span(name, fun) when is_atom(name) and is_function(fun, 0) do
    _ = start_span(name)

    try do
      fun.()
    after
      _ = end_span()
    end
  end

  # ============================================================================
  # Baggage (Additional Context)
  # ============================================================================

  @doc """
  Sets a baggage value that will be propagated with the trace.

  Baggage is additional context that travels with the trace.

  ## Examples

      Context.put_baggage(:user_id, "user123")
      Context.put_baggage(:request_path, "/api/users")
  """
  @spec put_baggage(atom(), any()) :: :ok
  def put_baggage(key, value) when is_atom(key) do
    case get() do
      nil ->
        :ok

      context ->
        new_baggage = Map.put(context.baggage, key, value)
        Process.put(@trace_key, %{context | baggage: new_baggage})
        :ok
    end
  end

  @doc """
  Gets a baggage value.
  """
  @spec get_baggage(atom(), any()) :: any()
  def get_baggage(key, default \\ nil) when is_atom(key) do
    case get() do
      nil -> default
      context -> Map.get(context.baggage, key, default)
    end
  end

  # ============================================================================
  # Telemetry Integration
  # ============================================================================

  @doc """
  Converts current context to telemetry metadata map.

  Use this to add trace context to telemetry events:

  ## Examples

      metadata = Context.to_metadata()
      :telemetry.execute([:raxol, :render], measurements, metadata)

      # With additional metadata
      metadata = Context.to_metadata(%{component: :button})
  """
  @spec to_metadata(map()) :: map()
  def to_metadata(extra \\ %{}) do
    case get() do
      nil ->
        extra

      context ->
        Map.merge(extra, %{
          trace_id: context.trace_id,
          span_id: context.span_id,
          parent_span_id: context.parent_span_id
        })
    end
  end

  @doc """
  Executes a telemetry event with trace context automatically injected.

  ## Examples

      Context.execute([:raxol, :render, :stop], %{duration: 1234}, %{component: :button})
  """
  @spec execute([atom()], map(), map()) :: :ok
  def execute(event, measurements, metadata \\ %{}) do
    enriched_metadata = to_metadata(metadata)
    :telemetry.execute(event, measurements, enriched_metadata)
  end

  @doc """
  Wraps a function with telemetry span instrumentation.

  Emits start/stop/exception events with trace context.

  ## Examples

      result = Context.span([:raxol, :render], %{component: :button}, fn ->
        render_component()
      end)
  """
  @spec span([atom()], map(), (-> result)) :: result when result: any()
  def span(event_prefix, metadata, fun)
      when is_list(event_prefix) and is_function(fun, 0) do
    span_name =
      event_prefix
      |> List.last()
      |> to_string()
      |> String.to_atom()

    _ = start_span(span_name)
    start_time = System.monotonic_time()

    enriched_metadata = to_metadata(metadata)

    _ =
      execute(
        event_prefix ++ [:start],
        %{system_time: System.system_time()},
        enriched_metadata
      )

    try do
      result = fun.()
      duration = System.monotonic_time() - start_time

      _ =
        execute(
          event_prefix ++ [:stop],
          %{duration: duration},
          Map.put(enriched_metadata, :result, :ok)
        )

      result
    rescue
      exception ->
        duration = System.monotonic_time() - start_time

        _ =
          execute(
            event_prefix ++ [:exception],
            %{duration: duration},
            Map.merge(enriched_metadata, %{
              exception: exception,
              stacktrace: __STACKTRACE__
            })
          )

        reraise exception, __STACKTRACE__
    after
      _ = end_span()
    end
  end

  # ============================================================================
  # Process Propagation
  # ============================================================================

  @doc """
  Captures current context for propagation to another process.

  ## Examples

      # In parent process
      captured = Context.capture()

      # In spawned process
      Task.async(fn ->
        Context.restore(captured)
        # ... work with trace context ...
      end)
  """
  @spec capture() :: t() | nil
  def capture do
    case get() do
      nil -> nil
      context -> Map.put(context, :span_stack, Process.get(@span_stack_key, []))
    end
  end

  @doc """
  Restores captured context in a new process.
  """
  @spec restore(map() | nil) :: :ok
  def restore(nil), do: :ok

  def restore(captured) when is_map(captured) do
    {span_stack, context} = Map.pop(captured, :span_stack, [])
    Process.put(@trace_key, context)
    Process.put(@span_stack_key, span_stack)
    :ok
  end

  # ============================================================================
  # Private Functions
  # ============================================================================

  defp generate_id do
    :crypto.strong_rand_bytes(8)
    |> Base.encode16(case: :lower)
  end

  defp get_grandparent_span_id([]), do: nil
  defp get_grandparent_span_id([{span_id, _name} | _rest]), do: span_id
end
