lib/arke/system.ex

# Copyright 2023 Arkemis S.r.l.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

defmodule Arke.System do
  defmacro __using__(_) do
    quote do
      #      @after_compile __MODULE__
      Module.register_attribute(__MODULE__, :arke, accumulate: false, persist: true)
      Module.register_attribute(__MODULE__, :groups, accumulate: true, persist: true)
      Module.register_attribute(__MODULE__, :parameters, accumulate: true, persist: false)

      Module.put_attribute(__MODULE__, :system_arke, true)

      import unquote(__MODULE__),
        only: [arke: 1, arke: 2, parameter: 3, parameter: 2, group: 1, group: 2]

      #      @before_compile unquote(__MODULE__)

      def arke_from_attr(),
        do: Keyword.get(__MODULE__.__info__(:attributes), :arke, []) |> List.first()

      def groups_from_attr(), do: Keyword.get(__MODULE__.__info__(:attributes), :groups, [])

      def base_parameters() do
        unit = arke_from_attr()
        unit.data.parameters
      end

      def on_load(data, _persistence_fn), do: {:ok, data}
      def before_load(data, _persistence_fn), do: {:ok, data}
      def on_validate(arke, unit), do: {:ok, unit}
      def before_validate(arke, unit), do: {:ok, unit}
      def on_create(arke, unit), do: {:ok, unit}
      def before_create(arke, unit), do: {:ok, unit}
      def on_struct_encode(_, _, data, opts), do: {:ok, data}
      def before_struct_encode(_, unit), do: {:ok, unit}
      def on_update(arke, old_unit, unit), do: {:ok, unit}
      def before_update(arke, unit), do: {:ok, unit}
      def on_delete(arke, unit), do: {:ok, unit}
      def before_delete(arke, unit), do: {:ok, unit}

      def after_get_struct(arke, unit, struct), do: struct
      def after_get_struct(arke, struct), do: struct

      def import(%{runtime_data: %{conn: %{method: "POST"}=conn}, metadata: %{project: project}} = arke) do
        member = ArkeAuth.Guardian.Plug.current_resource(conn)
        mode = Map.get(conn.body_params, "mode", "default")

        case Map.get(conn.body_params, "file", nil) do
          nil -> {:error, "file is required", 400}
          file -> import_units(arke, project, member, file, mode)
        end
      end

      defp import_units(arke, project, member, file, mode) do
        {:ok, ref} = Enum.at(Xlsxir.multi_extract(file.path), 0)
        all_units = get_all_units_for_import(project)

        file_as_list = Xlsxir.get_list(ref)

        header_file = Enum.at(file_as_list, 0)
        rows = file_as_list |> List.delete_at(0)

        header = get_header_for_import(project, arke, header_file) |> parse_haeder_for_import(header_file)

        {correct_units, error_units} = Enum.with_index(rows) |> Enum.reduce({[], []}, fn {row, index}, {correct_units, error_units} ->
          case Enum.filter(row, & !is_nil(&1)) do
            [] -> {correct_units, error_units}
            _ -> case load_units(project, arke, header, row, all_units, mode) do
                   {:error, args, errors} ->
                     m = Enum.reduce(header, %{}, fn {h, index}, acc ->
                       acc = Map.put(acc, h, parse_cell(Enum.at(row, index)))
                     end) |> Map.put("errors", errors)
                     {correct_units, [m | error_units]}
                   {:ok, unit_args} -> {[unit_args | correct_units], error_units}
                 end
          end
        end)

        existing_units = get_existing_units_for_import(project, arke, header, correct_units)
#        units_args =  Enum.reduce(correct_units, [], fn u, units_args ->
#          case check_existing_units_for_import(project, arke, header, u, existing_units) do
#            true -> units_args
#            false -> [u | units_args]
#          end
#        end)
        units_args = Enum.filter(correct_units, fn u -> check_existing_units_for_import(project, arke, header, u, existing_units) == false end)
        if length(units_args) > 0 do
          Enum.map(Stream.chunk_every(units_args, 5000) |> Enum.to_list(), fn chunk ->
            ArkePostgres.Repo.insert_all("arke_unit", chunk, prefix: Atom.to_string(project))
          end)
        end

        count_inserted =  length(units_args)
        count_existing =  length(existing_units)
        count_error =  length(error_units)
        total_count =  count_inserted + count_error + count_existing

        res = %{
          count_inserted: count_inserted,
          count_existing: count_existing,
          count_error: count_error,
          total_count: total_count,
          error_units: error_units
        }
        {:ok, res, 201}
      end

      defp parse_cell(value) when is_tuple(value), do: Kernel.inspect(value)
      defp parse_cell(value), do: value

      defp get_header_for_import(project, arke, header_file) do
        Enum.reduce(Enum.with_index(header_file), [], fn {cell, index}, acc ->
          case Arke.Boundary.ArkeManager.get_parameter(arke, project, cell) do
            nil -> acc
            parameter -> [Atom.to_string(parameter.id) | acc]
          end
        end)
      end
      defp parse_haeder_for_import(header, header_file) do
        Enum.reduce(Enum.with_index(header_file), [], fn {cell, index}, acc ->
          case cell do
            nil -> acc
            "" -> acc
            cell ->
              case cell in header do
                nil -> acc
                parameter -> [{cell, index} | acc]
              end
          end
        end)
      end

      defp get_all_units_for_import(project), do: []

      defp load_units(project, arke, header, row, _, "default") do
        args = Enum.reduce(header, [], fn {parameter_id, index}, acc ->
          acc = Keyword.put(acc, String.to_existing_atom(parameter_id), Enum.at(row, index))
        end)

        with %Arke.Core.Unit{} = unit <- Arke.Core.Unit.load(arke, args, :create),
             {:ok, unit} <- Arke.Validator.validate(unit, :create, project),
             do: {:ok, args},
             else: ({:error, errors} -> {:error, args, errors})
      end
      defp get_existing_units_for_import(project, arke, header, units_args), do: []
      defp check_existing_units_for_import(project, arke, header, units_args, existing_units), do: true
      defp get_import_value(header, row, column) do
        index = Enum.find(header, fn {k, v} -> k == column end) |> elem(1)
        Enum.at(row, index)
      end

      defoverridable on_load: 2,
                     before_load: 2,
                     on_validate: 2,
                     before_validate: 2,
                     on_create: 2,
                     before_create: 2,
                     before_struct_encode: 2,
                     on_struct_encode: 4,
                     on_update: 3,
                     before_update: 2,
                     on_delete: 2,
                     before_delete: 2,
                     after_get_struct: 2,
                     after_get_struct: 3,

                    # Import
                      import: 1,
                     import_units: 5,
                      get_header_for_import: 3,
                     get_all_units_for_import: 1,
                      load_units: 6,
                     get_existing_units_for_import: 4,
                      check_existing_units_for_import: 5
    end
  end

  #  defmacro __before_compile__(env) do
  #  end
  #
  #  def compile(translations) do
  #
  #  end

  ######################################################################################################################
  # ARKE MACRO #########################################################################################################
  ######################################################################################################################

  @doc """
  Macro to create an arke struct with the given parameters.
  Usable only via `code` and not `iex`.


  ## Example
      arke do
        parameter :custom_parameter, :string, required: true, unique: true
        parameter :custom_parameter2, :string, required: true, values: ["value1", "value2"]
        parameter :custom_parameter3, :integer, required: true, values: [%{label: "option 1", value: 1},%{label: "option 2", value: 2}]
        parameter :custom_parameter4, :dict, required: true, default: %{"default_dict_key": "default_dict_value"}
      end

  ## Return
      %Arke.Core.'{arke_struct}'{}

  """
  @spec arke(args :: list(), Macro.t()) :: %{}
  defmacro arke(opts \\ [], do: block) do
    type = Keyword.get(opts, :type, "arke")
    active = Keyword.get(opts, :active, true)
    metadata = Keyword.get(opts, :metadata, %{})
    remote = Keyword.get(opts, :remote, false)

    base_parameters = get_base_arke_parameters(type)

    quote do
      type = unquote(type)
      active = unquote(active)
      remote = unquote(remote)
      opts = unquote(opts)
      metadata = unquote(Macro.escape(metadata))
      caller = unquote(__CALLER__.module)

      id =
        Keyword.get(
          opts,
          :id,
          caller
          |> to_string
          |> String.split(".")
          |> List.last()
          |> Macro.underscore()
          |> String.to_atom()
        )

      label =
        Keyword.get(
          opts,
          :label,
          id |> Atom.to_string() |> String.replace("_", " ") |> String.capitalize()
        )

      unquote(base_parameters)
      unquote(block)

      @arke %{
        id: id,
        data: %{label: label, active: active, type: type, parameters: @parameters},
        metadata: metadata,
        remote: remote
      }
      #      @arke Arke.Core.Arke.new(id: id, label: label, active: active, metadata: metadata, type: type, parameters: @parameters)
    end
  end

  defp get_base_arke_parameters("arke") do
    quote do
      parameter(:id, :string, required: true, persistence: "table_column")
      parameter(:arke_id, :string, required: false, persistence: "table_column")
      parameter(:metadata, :dict, required: false, persistence: "table_column")
      parameter(:inserted_at, :datetime, required: false, persistence: "table_column")
      parameter(:updated_at, :datetime, required: false, persistence: "table_column")
    end
  end

  defp get_base_arke_parameters(_type), do: nil

  #  @spec __arke_info__(caller :: caller(), options :: list()) :: [id: atom() | String.t(), label: String.t(), active: boolean(), metadata: map(), type: atom()]
  #  defp __arke_info__(caller, options) do
  #
  #    id = Keyword.get(options, :id, caller |> to_string |> String.split(".") |> List.last |> Macro.underscore |> String.to_atom)
  #    label = Keyword.get(options, :label, id |> Atom.to_string |> String.replace("_", " ") |> String.capitalize)
  #    [
  #      id: id,
  #      label: label,
  #      active: Keyword.get(options, :active, true),
  #      metadata: Keyword.get(options, :metadata, %{}),
  #      type: Keyword.get(options, :type, :arke)
  #    ]
  #  end

  ######################################################################################################################
  # END ARKE MACRO #####################################################################################################
  ######################################################################################################################

  ######################################################################################################################
  # PARAMETER MACRO ####################################################################################################
  ######################################################################################################################

  @doc """
  Macro used to define parameter in an arke.
  See example above `arke/2`

  """
  @spec parameter(id :: atom(), type :: atom(), opts :: list()) :: Macro.t()
  defmacro parameter(id, type, opts \\ []) do
    # parameter_dict = Arke.System.BaseParameter.parameter_options(opts, id, type)
    quote bind_quoted: [id: id, type: type, opts: opts] do
      opts = Arke.System.BaseParameter.check_enum(type, opts)
      @parameters %{id: id, arke: type, metadata: opts}
    end
  end

  ######################################################################################################################
  # END PARAMETER MACRO ################################################################################################
  ######################################################################################################################

  ######################################################################################################################
  # GROUP MACRO ####################################################################################################
  ######################################################################################################################

  @doc """
  Macro used to define parameter in an arke.
  See example above `arke/2`

  """
  @spec group(id :: atom(), opts :: list()) :: Macro.t()
  defmacro group(id, opts \\ []) do
    quote bind_quoted: [id: id, opts: opts] do
      @groups %{id: id, metadata: opts}
    end
  end

  ######################################################################################################################
  # END GROUP MACRO ################################################################################################
  ######################################################################################################################
end

defmodule Arke.System.Arke do
  use Arke.System
end

defmodule Arke.System.BaseArke do
  defstruct [:id, :label, :active, :type, :parameters, :metadata]
end

defmodule Arke.System.BaseParameter do
  defstruct [:id, :label, :active, :metadata, :type, :parameters]

  @doc """
  Used in the parameter macro to create the map for every parameter which have the `values` option.
  It check if the given value are the same type as the parameter type and then creates a  list of map as follows:

       [%{label "given label", value: given_value}, %{label "given label two ", value: given_value_two}]

  Keep in mind that if the values are declared as list instead of map the label will be generated from the value itself.
       ... omitted code

            parameter :custom_parameter2, :integer, required: true, values: [1, 2, 3]

       ... omitted code
    The code above will results in an `{arke_struct}` with the values as follows

        ... omitted code

            values: [%{label "1", value: 1}, %{label "2", value: 2}, %{label "3", value: 3}]

        ... omitted code

  """
  @spec parameter_options(opts :: list(), id :: atom(), type :: atom()) :: %{
          type: atom(),
          opts: list()
        }
  def parameter_options(opts, id, type) do
    opts =
      opts
      |> parameter_option_common(id)
      |> parameter_by_type(type)

    %{type: type, opts: opts}
  end

  def check_enum(type, opts) do
    enum_parameters = [:string, :integer, :float]

    case type in enum_parameters do
      true -> __enum_parameter__(opts, type)
      false -> opts
    end
  end

  defp parameter_option_common(opts, id) do
    opts
    |> Keyword.put(:id, id)
    |> Keyword.put(
      :label,
      Keyword.get(
        opts,
        :label,
        id |> Atom.to_string() |> String.replace("_", " ") |> String.capitalize()
      )
    )
    |> parameter_option(:required, false)
    |> parameter_option(:nullable, true)
    |> parameter_option(:default, nil)
    |> parameter_option(:persistence, "arke_parameter")
  end

  defp parameter_by_type(opts, :string) do
    opts
    |> parameter_option(:type, :string)
    |> parameter_option(:min_length, nil)
    |> parameter_option(:max_length, nil)
    |> parameter_option(:strip, nil)
    |> __enum_parameter__(:string)
  end

  defp parameter_by_type(opts, :integer) do
    opts
    |> parameter_option(:type, :integer)
    |> __number_parameter__()
    |> __enum_parameter__(:integer)
  end

  defp parameter_by_type(opts, :float) do
    opts
    |> parameter_option(:type, :float)
    |> __number_parameter__()
    |> __enum_parameter__(:float)
  end

  defp parameter_by_type(opts, type), do: opts |> parameter_option(:type, type)

  defp __number_parameter__(opts) do
    opts
    |> parameter_option(:min, nil)
    |> parameter_option(:max, nil)
  end

  defp parameter_option(opts, key, default) do
    Keyword.put_new(opts, key, default)
  end

  defp __enum_parameter__(opts, type) do
    case Keyword.has_key?(opts, :values) do
      true -> __validate_values__(opts, opts[:values], type)
      false -> opts
    end
  end

  defp __validate_values__(opts, nil, _), do: Keyword.delete(opts, :values)

  defp __validate_values__(opts, %{"value" => value, "datetime" => _} = values, type)
       when not is_nil(value),
       do: __validate_values__(opts, value, type)

  defp __validate_values__(opts, [h | _t] = values, type) when is_map(h) do
    condition =
      cond do
        type == :string ->
          fn l, v -> (is_binary(l) and is_binary(v)) or (is_atom(l) and is_atom(v)) end

        type == :integer ->
          fn l, v -> is_binary(l) and is_integer(v) end

        type == :float ->
          fn l, v -> is_binary(l) and is_number(v) end
      end

    case Enum.all?(values, fn map ->
           Enum.map([:label, :value], fn key -> Map.has_key?(map, key) end)
         end) do
      true ->
        __create_map_values__(__check_map__(values), opts, type, condition)

      # FARE RAISE ECCEZIONE DA GESTIRE. CHIAVI DEVONO ESSERE TUTTE UGUALI
      _ ->
        Keyword.update(opts, :values, nil, fn _current_value -> nil end)
    end
  end

  defp __validate_values__(opts, values, type) do
    condition =
      cond do
        type == :string -> fn v -> is_binary(v) or is_atom(v) end
        type == :integer -> fn v -> is_integer(v) end
        type == :float -> fn v -> is_number(v) end
      end

    __values_from_list__(values, opts, condition)
  end

  # FARE RAISE ECCEZIONE DA GESTIRE
  defp __validate_values__(opts, _, _),
    do: Keyword.update(opts, :values, nil, fn _current_value -> nil end)

  # CONVERT ALL STRINGS KEY TO ATOMS (string are received from API)
  defp __check_map__([%{"label" => _l, "value" => _v} | h] = values) do
    Enum.map(
      values,
      &Enum.into(&1, %{}, fn {key, val} -> {String.to_existing_atom(key), val} end)
    )
  end

  defp __check_map__(values), do: values

  defp __create_map_values__(values, opts, type, condition) do
    # FARE RAISE ECCEZIONE DA GESTIRE. CHIAVI DEVONO ESSERE TUTTE UGUALI
    with true <- Enum.all?(values, fn %{label: l, value: v} -> condition.(l, v) end) do
      new_values =
        Enum.map(values, fn k ->
          %{label: String.capitalize(to_string(k.label)), value: __get_map_value__(k.value, type)}
        end)

      __create_index__(opts, new_values)
    else
      _ -> Keyword.update(opts, :values, nil, fn _current_value -> nil end)
    end
  end

  defp __get_map_value__(value, :string), do: to_string(value)
  defp __get_map_value__(value, _), do: value

  defp __values_from_list__(values, opts, condition) do
    # FARE RAISE ECCEZIONE DA GESTIRE. CHIAVI DEVONO ESSERE TUTTE UGUALI
    with true <- Enum.all?(values, &condition.(&1)) do
      new_values =
        Enum.map(values, fn k -> %{label: String.capitalize(to_string(k)), value: k} end)

      __create_index__(opts, new_values)
    else
      _ -> Keyword.update(opts, :values, nil, fn _current_value -> nil end)
    end
  end

  defp __create_index__(opts, new_values),
    do: Keyword.delete(opts, :values) |> Keyword.put_new(:values, new_values)
end