lib/qr_nbu/error.ex

defmodule QRNBU.Error do
  @moduledoc """
  Structured error types for NBU QR code library.

  This module provides consistent error handling with categorized error types,
  field information, and error codes for programmatic handling.

  ## Error Categories

  - `:validation_error` - Data validation failures
  - `:encoding_error` - Character encoding failures
  - `:format_error` - Output formatting failures
  - `:unsupported_error` - Unsupported feature/version requests

  ## Examples

      iex> error = QRNBU.Error.validation_error(:iban, "Invalid checksum")
      iex> raise error
      ** (QRNBU.Error) Validation error on field 'iban': Invalid checksum

      iex> QRNBU.Error.encoding_error("Failed to encode to CP1251")
      %QRNBU.Error{code: :encoding_error, message: "Encoding error: Failed to encode to CP1251", field: nil, details: %{}}
  """

  defexception [:message, :field, :code, :details]

  @type t :: %__MODULE__{
          message: String.t(),
          field: atom() | nil,
          code: error_code(),
          details: map() | nil
        }

  @type error_code ::
          :validation_error
          | :encoding_error
          | :format_error
          | :unsupported_error

  @doc """
  Creates a validation error for a specific field.

  ## Examples

      iex> QRNBU.Error.validation_error(:iban, "Must be 29 characters")
      %QRNBU.Error{
        code: :validation_error,
        field: :iban,
        message: "Validation error on field 'iban': Must be 29 characters",
        details: %{}
      }
  """
  @spec validation_error(atom(), String.t(), map()) :: t()
  def validation_error(field, message, details \\ %{}) do
    %__MODULE__{
      message: "Validation error on field '#{field}': #{message}",
      field: field,
      code: :validation_error,
      details: details
    }
  end

  @doc """
  Creates an encoding error.

  ## Examples

      iex> QRNBU.Error.encoding_error("CP1251 encoding failed")
      %QRNBU.Error{
        code: :encoding_error,
        message: "Encoding error: CP1251 encoding failed",
        field: nil,
        details: %{}
      }
  """
  @spec encoding_error(String.t(), map()) :: t()
  def encoding_error(message, details \\ %{}) do
    %__MODULE__{
      message: "Encoding error: #{message}",
      field: nil,
      code: :encoding_error,
      details: details
    }
  end

  @doc """
  Creates a formatting error.

  ## Examples

      iex> QRNBU.Error.format_error("Invalid line ending for V003")
      %QRNBU.Error{
        code: :format_error,
        message: "Format error: Invalid line ending for V003",
        field: nil,
        details: %{}
      }
  """
  @spec format_error(String.t(), map()) :: t()
  def format_error(message, details \\ %{}) do
    %__MODULE__{
      message: "Format error: #{message}",
      field: nil,
      code: :format_error,
      details: details
    }
  end

  @doc """
  Creates an unsupported feature/version error.

  ## Examples

      iex> QRNBU.Error.unsupported_error("Version 4 is not supported")
      %QRNBU.Error{
        code: :unsupported_error,
        message: "Unsupported: Version 4 is not supported",
        field: nil,
        details: %{}
      }
  """
  @spec unsupported_error(String.t(), map()) :: t()
  def unsupported_error(message, details \\ %{}) do
    %__MODULE__{
      message: "Unsupported: #{message}",
      field: nil,
      code: :unsupported_error,
      details: details
    }
  end

  @doc """
  Converts error to a simple string message.
  """
  @spec to_message(t()) :: String.t()
  def to_message(%__MODULE__{message: message}), do: message

  @doc """
  Wraps multiple error messages into a formatted string.
  """
  @spec format_errors([t()]) :: String.t()
  def format_errors(errors) when is_list(errors) do
    error_lines =
      errors
      |> Enum.map(fn
        %__MODULE__{field: nil, message: message} -> "- #{message}"
        %__MODULE__{field: field, message: message} -> "- #{field}: #{message}"
      end)
      |> Enum.join("\n")

    "Multiple validation errors:\n#{error_lines}"
  end

  # Exception protocol implementation
  @impl true
  def message(%__MODULE__{message: message}), do: message
end