lib/selecto/error.ex

defmodule Selecto.Error do
  @moduledoc """
  Standardized error structure for all Selecto operations.

  Provides consistent error handling across the Selecto ecosystem with
  structured error information including context, query details, and
  actionable error types.

  ## Error Types

  - `:connection_error` - Database connection failures
  - `:query_error` - SQL query execution failures
  - `:validation_error` - Input validation failures
  - `:configuration_error` - Invalid domain or Selecto configuration
  - `:no_results` - Query returned no results when one expected
  - `:multiple_results` - Query returned multiple results when one expected
  - `:timeout_error` - Query execution timeout

  ## Examples

      # Connection error
      {:error, %Selecto.Error{
        type: :connection_error,
        message: "Failed to connect to database",
        details: %{host: "localhost", port: 5432}
      }}

      # Query error with context
      {:error, %Selecto.Error{
        type: :query_error,
        message: "Column 'invalid_col' does not exist",
        query: "SELECT invalid_col FROM users",
        params: [],
        details: %{column: "invalid_col", table: "users"}
      }}
  """

  defstruct [:type, :message, :details, :query, :params]

  @type t :: %__MODULE__{
          type: error_type(),
          message: String.t(),
          details: map() | nil,
          query: String.t() | nil,
          params: [term()] | nil
        }

  @type error_type ::
          :connection_error
          | :query_error
          | :validation_error
          | :configuration_error
          | :no_results
          | :multiple_results
          | :timeout_error
          | :field_resolution_error
          | :transformation_error

  @doc """
  Creates a connection error.
  """
  @spec connection_error(String.t(), map()) :: t()
  def connection_error(message, details \\ %{}) do
    %__MODULE__{
      type: :connection_error,
      message: message,
      details: details
    }
  end

  @doc """
  Creates a query execution error with SQL context.
  """
  @spec query_error(String.t(), String.t() | nil, [term()], map()) :: t()
  def query_error(message, query \\ nil, params \\ [], details \\ %{}) do
    %__MODULE__{
      type: :query_error,
      message: message,
      query: query,
      params: params,
      details: details
    }
  end

  @doc """
  Creates a validation error.
  """
  @spec validation_error(String.t(), map()) :: t()
  def validation_error(message, details \\ %{}) do
    %__MODULE__{
      type: :validation_error,
      message: message,
      details: details
    }
  end

  @doc """
  Creates a configuration error.
  """
  @spec configuration_error(String.t(), map()) :: t()
  def configuration_error(message, details \\ %{}) do
    %__MODULE__{
      type: :configuration_error,
      message: message,
      details: details
    }
  end

  @doc """
  Creates a no results error for execute_one/2.
  """
  @spec no_results_error(String.t()) :: t()
  def no_results_error(message \\ "Query returned no results") do
    %__MODULE__{
      type: :no_results,
      message: message
    }
  end

  @doc """
  Creates a multiple results error for execute_one/2.
  """
  @spec multiple_results_error(String.t()) :: t()
  def multiple_results_error(message \\ "Query returned multiple results when one expected") do
    %__MODULE__{
      type: :multiple_results,
      message: message
    }
  end

  @doc """
  Creates a timeout error.
  """
  @spec timeout_error(String.t(), map()) :: t()
  def timeout_error(message, details \\ %{}) do
    %__MODULE__{
      type: :timeout_error,
      message: message,
      details: details
    }
  end

  @doc """
  Creates a query generation error.
  """
  @spec query_generation_error(String.t(), map()) :: t()
  def query_generation_error(message, details \\ %{}) do
    %__MODULE__{
      type: :query_error,
      message: message,
      details: details
    }
  end

  @doc """
  Creates a field resolution error with context.
  """
  @spec field_resolution_error(String.t(), term(), map()) :: t()
  def field_resolution_error(message, field_ref, context \\ %{}) do
    %__MODULE__{
      type: :field_resolution_error,
      message: message,
      details: Map.merge(context, %{field_reference: field_ref})
    }
  end

  @doc """
  Creates a transformation error for output format processing.
  """
  @spec transformation_error(String.t(), map()) :: t()
  def transformation_error(message, details \\ %{}) do
    %__MODULE__{
      type: :transformation_error,
      message: message,
      details: details
    }
  end

  @doc """
  Converts various error types to standardized Selecto.Error.
  """
  @spec from_reason(term()) :: t()
  def from_reason({:exit, reason}) do
    connection_error("Database connection failed", %{reason: reason})
  end

  def from_reason(%{__exception__: true, message: message} = exception) do
    query_error(message, nil, [], %{exception: exception})
  end

  def from_reason(:no_results) do
    no_results_error()
  end

  def from_reason(:multiple_results) do
    multiple_results_error()
  end

  def from_reason(reason) when is_binary(reason) do
    query_error(reason)
  end

  def from_reason(reason) do
    query_error("Execution failed", nil, [], %{reason: reason})
  end

  @doc """
  Converts a Selecto.Error to an exception for raising.
  """
  @spec to_exception(t()) :: Exception.t()
  def to_exception(%__MODULE__{type: :connection_error, message: message}) do
    RuntimeError.exception("Database connection failed: #{message}")
  end

  def to_exception(%__MODULE__{message: message}) do
    RuntimeError.exception(message)
  end

  @doc """
  Creates a user-friendly error message for display.
  """
  @spec to_display_message(t()) :: String.t()
  def to_display_message(%__MODULE__{type: :connection_error, message: message}) do
    "Database connection failed: #{message}"
  end

  def to_display_message(%__MODULE__{type: :query_error, message: message}) do
    "Query execution failed: #{message}"
  end

  def to_display_message(%__MODULE__{type: :validation_error, message: message}) do
    "Validation error: #{message}"
  end

  def to_display_message(%__MODULE__{type: :configuration_error, message: message}) do
    "Configuration error: #{message}"
  end

  def to_display_message(%__MODULE__{type: :no_results}) do
    "No results found"
  end

  def to_display_message(%__MODULE__{type: :multiple_results}) do
    "Expected one result, but got multiple"
  end

  def to_display_message(%__MODULE__{type: :timeout_error, message: message}) do
    "Query timeout: #{message}"
  end

  def to_display_message(%__MODULE__{type: :field_resolution_error, message: message}) do
    "Field resolution error: #{message}"
  end

  def to_display_message(%__MODULE__{type: :transformation_error, message: message}) do
    "Output format transformation error: #{message}"
  end

  def to_display_message(%__MODULE__{message: message}) do
    message
  end
end