defmodule Phoenix.Sync.Writer.Transaction do
  @moduledoc """
  Represents a transaction containing a list of `Phoenix.Sync.Writer.Operation`s
  that should be applied atomically.

  ```elixir
  {:ok, operations} <- Transaction.parse_operations(operations, &parse_operation/1)

  %Transaction{} = Transaction.new(operations)
  ```
  """

  defstruct txid: nil, operations: []

  alias Phoenix.Sync.Writer.Operation

  @type id() :: integer()
  @type t() :: %__MODULE__{
          txid: nil | id(),
          operations: [Operation.t(), ...]
        }

  @doc """
  Return a new, empty, `Transaction` struct.
  """
  @spec empty() :: t()
  def empty, do: %__MODULE__{}

  @spec new([Operation.t()]) :: {:ok, t()} | {:error, term()}
  def new([]) do
    {:error, "empty transaction"}
  end

  def new(operations) when is_list(operations) do
    if Enum.all?(operations, &match?(%Operation{}, &1)) do
      operations =
        operations
        |> Enum.with_index()
        |> Enum.map(fn {operation, idx} -> %{operation | index: idx} end)

      {:ok, %__MODULE__{operations: operations}}
    else
      {:error, "Invalid operations list"}
    end
  end

  defdelegate operation(operation, table, data, changes), to: Operation, as: :new
  defdelegate operation!(operation, table, data, changes), to: Operation, as: :new!

  @doc """
  Helper function to parse a list of encoded Operations.
  """
  @spec parse_operations([term()], (term() -> {:ok, Operation.t()} | {:error, term()})) ::
          {:ok, [Operation.t()]} | {:error, term()}
  def parse_operations(raw_operations, parse_function) when is_function(parse_function, 1) do
    with operations when is_list(operations) <-
           do_parse_operations(raw_operations, parse_function) do
      {:ok, Enum.reverse(operations)}
    end
  end

  defp do_parse_operations(raw_operations, parse_function) do
    raw_operations
    |> Enum.reduce_while([], fn raw_op, operations ->
      case parse_function.(raw_op) do
        {:ok, operation} ->
          {:cont, [operation | operations]}

        {:error, _reason} = error ->
          {:halt, error}
      end
    end)
  end
end
