defmodule Funx.Monad.Effect.Context do
  @moduledoc """
  Represents the execution context attached to an effect.

  This struct carries contextual information such as `trace_id`, `span_name`,
  timeouts, and arbitrary metadata (`baggage` and `metadata`). It supports
  telemetry integration, span linking, and timeout control, and is propagated
  automatically across composed effects.

  Developers can set fields like `timeout`, `trace_id`, or `span_name` when
  constructing `Left` and `Right` effects. The context is merged or promoted
  as needed when chaining effects to preserve trace continuity and execution scope.

  This context is not injected at runtime via `run/2`—it is bound to the effect
  when created.
  """

  import Funx.Foldable, only: [fold_l: 3]
  alias Funx.Predicate

  @enforce_keys [:trace_id]
  defstruct [
    :trace_id,
    :parent_trace_id,
    :span_name,
    :timeout,
    baggage: %{},
    metadata: %{}
  ]

  @typedoc """
  Represents the contextual metadata associated with an `Effect`.

  The context carries telemetry and tracing information such as `trace_id`, `span_name`, and `timeout`,
  as well as arbitrary metadata and user-defined `baggage`. It is passed through all effectful computations
  and can be promoted to represent nested spans.
  """
  @type t :: %__MODULE__{
          trace_id: String.t(),
          parent_trace_id: String.t() | nil,
          span_name: String.t() | nil,
          timeout: non_neg_integer() | nil,
          baggage: map() | nil,
          metadata: map() | nil
        }

  @typedoc """
  Represents input for constructing an `Effect.Context`.

  Accepts either a keyword list of options (`trace_id`, `span_name`, etc.) or an existing `Context` struct.
  Used throughout the `Effect` system for flexible context propagation.
  """
  @type opts_or_context :: keyword() | t()

  @doc """
  Creates a new `Funx.Monad.Effect.Context` struct for use with effectful computations.

  If no `:trace_id` is provided, a unique one is generated automatically.
  You may also set optional fields such as `:span_name`, `:timeout`, `:baggage`, and `:metadata`.

  The returned context is intended to be passed into `Left` and `Right` effects,
  where it will be propagated and updated across chained computations.

  ## Examples

      iex> ctx = Funx.Monad.Effect.Context.new(span_name: "load-data", timeout: 2000)
      iex> ctx.span_name
      "load-data"

      iex> ctx = Funx.Monad.Effect.Context.new(trace_id: "abc123")
      iex> ctx.trace_id
      "abc123"
  """
  @spec new(keyword() | t()) :: t()
  def new, do: new([])

  def new(%__MODULE__{} = trace) do
    %__MODULE__{
      trace_id: trace.trace_id || generate_trace_id(),
      parent_trace_id: trace.parent_trace_id,
      span_name: trace.span_name || Funx.Config.default_span_name(),
      timeout: trace.timeout,
      baggage: trace.baggage
    }
  end

  def new(fields) when is_list(fields) do
    trace_id = Keyword.get(fields, :trace_id, generate_trace_id())
    span_name = Keyword.get(fields, :span_name, Funx.Config.default_span_name())

    %__MODULE__{
      trace_id: trace_id,
      parent_trace_id: Keyword.get(fields, :parent_trace_id),
      span_name: span_name,
      timeout: Keyword.get(fields, :timeout),
      baggage: Keyword.get(fields, :baggage, %{}),
      metadata: Keyword.get(fields, :metadata, %{})
    }
  end

  @doc """
  Merges two `%Funx.Monad.Effect.Context{}` structs into one, preferring non-nil values from the first context.

  This is used to preserve trace continuity and propagate context across composed effects.

  - Non-nil fields from the first context take precedence.
  - `baggage` and `metadata` maps are deeply merged.
  - This operation is idempotent and safe for reuse across nested effect chains.

  ## Examples

      iex> c1 = Funx.Monad.Effect.Context.new(trace_id: "a", baggage: %{user: 1})
      iex> c2 = Funx.Monad.Effect.Context.new(trace_id: "b", baggage: %{region: "us-west"})
      iex> Funx.Monad.Effect.Context.merge(c1, c2).baggage
      %{user: 1, region: "us-west"}
  """
  @spec merge(t(), t()) :: t()
  def merge(context1, context2) do
    %__MODULE__{
      trace_id: context1.trace_id || context2.trace_id,
      parent_trace_id: context1.parent_trace_id || context2.parent_trace_id,
      span_name: context1.span_name || context2.span_name,
      timeout: context1.timeout || context2.timeout,
      baggage: Map.merge(context2.baggage || %{}, context1.baggage || %{}),
      metadata: Map.merge(context2.metadata || %{}, context1.metadata || %{})
    }
  end

  @doc """
  Returns a new `%Funx.Monad.Effect.Context{}` with fields overridden by values from the given keyword list.

  - Direct fields like `:trace_id`, `:parent_trace_id`, `:span_name`, and `:timeout` are replaced if present.
  - Nested maps `:baggage` and `:metadata` are deeply merged, with the keyword list taking precedence.

  This is useful for refining or extending an existing context in a specific part of an effect chain.

  ## Examples

      iex> ctx = Funx.Monad.Effect.Context.new(trace_id: "abc", baggage: %{x: 1}, metadata: %{debug: false})
      iex> updated = Funx.Monad.Effect.Context.override(ctx, span_name: "child", baggage: %{x: 2}, metadata: %{debug: true})
      iex> updated.span_name
      "child"
      iex> updated.baggage
      %{x: 2}
      iex> updated.metadata
      %{debug: true}
  """
  @spec override(t(), keyword()) :: t()
  def override(%__MODULE__{} = context, opts) when is_list(opts) do
    %__MODULE__{
      trace_id: Keyword.get(opts, :trace_id, context.trace_id),
      parent_trace_id: Keyword.get(opts, :parent_trace_id, context.parent_trace_id),
      span_name: Keyword.get(opts, :span_name, context.span_name),
      timeout: Keyword.get(opts, :timeout, context.timeout),
      baggage: Map.merge(context.baggage || %{}, Keyword.get(opts, :baggage, %{})),
      metadata: Map.merge(context.metadata || %{}, Keyword.get(opts, :metadata, %{}))
    }
  end

  @doc """
  Promotes the current context into a child trace by generating a new `trace_id` and linking to the original.

  - The current `trace_id` is moved to `parent_trace_id`.
  - A new `trace_id` is generated for the child context.
  - The given `label` is prepended to the existing `span_name` as `"label -> span"`.

  This is typically used to represent a nested span or sub-operation within a larger effect chain,
  preserving trace lineage across composed effects.

  ## Examples

      iex> parent = Funx.Monad.Effect.Context.new(trace_id: "abc123", span_name: "load")
      iex> child = Funx.Monad.Effect.Context.promote_trace(parent, "decode")
      iex> child.parent_trace_id
      "abc123"
      iex> child.trace_id != "abc123"
      true
      iex> child.span_name
      "decode -> load"
  """
  @spec promote_trace(t(), String.t()) :: t()
  def promote_trace(%__MODULE__{} = context, label) do
    %__MODULE__{
      trace_id: generate_trace_id(),
      parent_trace_id: context.trace_id,
      span_name: "#{label} -> #{context.span_name || Funx.Config.default_span_name()}",
      timeout: context.timeout,
      baggage: context.baggage,
      metadata: context.metadata
    }
  end

  @doc """
  Generates a random lowercase hexadecimal trace ID.

  This function is used internally to ensure each trace is uniquely identifiable.

  ## Examples

      iex> id = Funx.Monad.Effect.Context.generate_trace_id()
      iex> String.length(id)
      32
      iex> id =~ ~r/^[a-f0-9]+$/
      true
  """
  @spec generate_trace_id() :: String.t()
  def generate_trace_id do
    :crypto.strong_rand_bytes(16) |> Base.encode16(case: :lower)
  end

  def span_name?(%__MODULE__{span_name: nil}), do: false
  def span_name?(%__MODULE__{}), do: true

  def default_span_name?(%__MODULE__{span_name: name}),
    do: name == Funx.Config.default_span_name()

  def empty_or_default_span_name?(%__MODULE__{} = context) do
    Predicate.p_any([
      Predicate.p_not(&span_name?/1),
      &default_span_name?/1
    ]).(context)
  end

  def default_span_name_if_empty(%__MODULE__{} = context, default_name) do
    fold_l(
      fn -> Predicate.p_not(&empty_or_default_span_name?/1).(context) end,
      fn -> context end,
      fn -> %__MODULE__{context | span_name: default_name} end
    )
  end
end
