lib/qr_nbu/version.ex

defmodule QRNBU.Version do
  @moduledoc """
  Behaviour definition for NBU QR code version implementations.

  This behaviour defines the contract that all version modules (V001, V002, V003)
  must implement. It ensures consistency across different QR code formats while
  allowing version-specific implementations.

  ## Implementing Modules

  - `QRNBU.V001` - Legacy format (9 fields)
  - `QRNBU.V002` - URL-based format (9 fields)
  - `QRNBU.V003` - Latest format (17 fields)

  ## Callbacks

  All version modules must implement:

  - `version_number/0` - Returns the version number (1, 2, or 3)
  - `validate/1` - Validates struct data according to version rules
  - `to_data_string/2` - Converts struct to NBU-formatted string
  - `from_data_string/2` - Parses NBU-formatted string to struct (optional for V001/V002)

  ## Examples

      defmodule QRNBU.V002 do
        @behaviour QRNBU.Version

        def version_number, do: 2

        def validate(%__MODULE__{} = data) do
          # Validation logic
        end

        def to_data_string(%__MODULE__{} = data, opts) do
          # Formatting logic
        end
      end
  """

  @type version_data :: struct()
  @type options :: keyword()
  @type error :: {:error, String.t() | [String.t()]}

  @doc """
  Returns the version number for this implementation.

  Must return 1 for V001, 2 for V002, or 3 for V003.
  """
  @callback version_number() :: 1 | 2 | 3

  @doc """
  Validates the struct data according to NBU specification for this version.

  Should validate:
  - Required fields are present
  - Field lengths are within limits
  - Field formats are correct (IBAN, amounts, etc.)
  - Version-specific constraints (e.g., ICT only in V003)

  Returns `{:ok, validated_data}` with potentially normalized data,
  or `{:error, reasons}` with validation error messages.
  """
  @callback validate(version_data()) :: {:ok, version_data()} | error()

  @doc """
  Converts struct data to NBU-formatted string ready for QR encoding.

  Options may include:
  - `:encoding` - `:utf8` or `:cp1251` (default: `:utf8`)
  - `:format` - `:raw` or `:url` (V002/V003 only, default: `:url`)
  - `:line_ending` - `:crlf` or `:lf` (auto-detected from version by default)

  Returns `{:ok, data_string}` with formatted QR data,
  or `{:error, reason}` if formatting fails.
  """
  @callback to_data_string(version_data(), options()) :: {:ok, String.t()} | error()

  @doc """
  Parses an NBU-formatted string back into struct data.

  Optional callback - primarily needed for V003 decoding.
  V001/V002 may implement basic parsing for compatibility.

  Options may include:
  - `:encoding` - Expected character encoding
  - `:strict` - Whether to enforce strict validation (default: true)

  Returns `{:ok, struct}` with parsed data,
  or `{:error, reason}` if parsing fails.
  """
  @callback from_data_string(String.t(), options()) :: {:ok, version_data()} | error()

  @optional_callbacks from_data_string: 2

  @doc """
  Helper function to determine version from version number.

  ## Examples

      iex> QRNBU.Version.module_for_version(1)
      {:ok, QRNBU.Versions.V001}

      iex> QRNBU.Version.module_for_version(2)
      {:ok, QRNBU.Versions.V002}

      iex> QRNBU.Version.module_for_version(3)
      {:ok, QRNBU.Versions.V003}

      iex> {:error, msg} = QRNBU.Version.module_for_version(4)
      iex> String.contains?(msg, "Unsupported version: 4")
      true
  """
  @spec module_for_version(pos_integer()) :: {:ok, module()} | {:error, String.t()}
  def module_for_version(1), do: {:ok, QRNBU.Versions.V001}
  def module_for_version(2), do: {:ok, QRNBU.Versions.V002}
  def module_for_version(3), do: {:ok, QRNBU.Versions.V003}

  def module_for_version(version),
    do: {:error, "Unsupported version: #{version}. Supported versions: 1, 2, 3"}

  @doc """
  Helper to validate data using the appropriate version module.

  Automatically determines which version module to use based on struct type.

  See individual version module tests for examples.
  """
  @spec validate(version_data()) :: {:ok, version_data()} | error()
  def validate(%module{} = data) do
    if function_exported?(module, :validate, 1) do
      module.validate(data)
    else
      {:error, "Module #{inspect(module)} does not implement QRNBU.Version behaviour"}
    end
  end

  @doc """
  Helper to convert data to string using the appropriate version module.

  Automatically determines which version module to use based on struct type.

  See individual version module tests for examples.
  """
  @spec to_data_string(version_data(), options()) :: {:ok, String.t()} | error()
  def to_data_string(%module{} = data, opts \\ []) do
    if function_exported?(module, :to_data_string, 2) do
      module.to_data_string(data, opts)
    else
      {:error, "Module #{inspect(module)} does not implement QRNBU.Version behaviour"}
    end
  end
end