lib/arke/query_manager.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.QueryManager do
  @moduledoc """
  Module to manage the CRUD operations to create the below elements and also manage the query to get the elements from db.

  ## `arke`
    - Parameter => `Arke.Core.Parameter`
    - Arke => `Arke.Core.Arke`
    - Unit => `Arke.Core.Unit`
    - Group => `Arke.Core.Group`

  ## `operators`
    - eq => equal => `=`
    - contains => contains a value (Case sensitive) =>  `LIKE %word%`
    - icontains => contains a value (Not case sensitive) => `LIKE %word%`
    - startswith => starts with the given value (Case sensitive) => `LIKE %word`
    - istartswith => starts with the given value (Not case sensitive) => `LIKE %word`
    - endswith => ends with the given value (Case sensitive) => `LIKE word%`
    - iendswith => ends with the given value (Not case sensitive) => `LIKE word%`
    - lte => less than or equal => `<=`
    - lt => less than => `<`
    - gt => greater than => `>`
    - gte => greater than or equal => `>=`
    - in =>  value is in a collection => `IN`

  """
  alias Arke.Boundary.{ArkeManager, ParameterManager, GroupManager}
  alias Arke.Validator
  alias Arke.LinkManager
  alias Arke.QueryManager
  alias Arke.Core.{Arke, Unit, Query, Parameter}

  @persistence Application.get_env(:arke, :persistence)
  @record_fields [:id, :data, :metadata, :inserted_at, :updated_at]

  @type func_return() :: {:ok, Unit.t()} | Error.t()
  @type operator() ::
          :eq
          | :contains
          | :icontains
          | :startswith
          | :istartswith
          | :endswith
          | :iendswith
          | :lte
          | :lt
          | :gt
          | :gte
          | :in
          | :isnull

  @doc """
  Create a new query

  ## Parameter
    - opts => %{map} || [keyword: value] || key1: value1, key2: value2 => map containing the project and the arke where to apply the query

  ## Example
      iex> Arke.QueryManager.query(project: :public)
  """
  @spec query(list()) :: Query.t()
  def query(opts) do
    project = Keyword.get(opts, :project)
    arke = get_arke(Keyword.get(opts, :arke), project)
    query = Query.new(arke, project)
  end

  @doc """
  Create a new topology query

  ## Parameter
    - query => refer to `query/1`
    - unit => %{arke_struct} => struct of the unit used as reference for the query
    - opts => [keyword: value] => KeywordList containing the link conditions
    - depth => int => max depth of the topoplogy
    - direction => :atom => :child/:parent => the direction of the link. From parent to child or viceversa
    - connection_type => string => name of the connection where to search

  ## Example

      iex> Arke.QueryManager.query(project: :public)
      ...> unit = QueryManager.get_by([project: :arke_system, id: "test"])
      ...> QueryManager.link(query, unit)

  ## Return
      %Arke.Core.Query{}
  """

  @spec link(Query.t(), unit :: Unit.t(), opts :: list()) :: Query.t()
  def link(query, unit, opts \\ []) do
    direction = Keyword.get(opts, :direction, :child)
    depth = Keyword.get(opts, :depth, 500)
    type = Keyword.get(opts, :type, nil)

    query
    |> Query.add_link_filter(unit, parse_link_depth(depth), parse_link_direction(direction), type)
  end

  defp parse_link_depth(depth) when is_binary(depth), do: String.to_integer(depth)
  defp parse_link_depth(depth) when is_integer(depth), do: depth

  # TODO exception if depth is not an integer
  defp parse_link_depth(depth), do: 500

  defp parse_link_direction(direction) when is_binary(direction),
    do: String.to_existing_atom(direction)

  defp parse_link_direction(direction), do: direction

  @doc """
  Function to create an element

  ## Parameters
    - project => :atom =>  identify the `Arke.Core.Project`
    - arke => {arke_struct} =>  identify the struct of the element we want to create
    - args => [list] =>  list of key: value we want to assign to the {arke_struct} above

  ## Example
      iex> string = ArkeManager.get(:string, :default)
      ...> Arke.QueryManager.create(:default, string, [id: "name", label: "Nome"])

  ## Returns
      {:ok, %Arke.Core.Unit{}}


  """
  @spec create(project :: atom(), arke :: Arke.t(), args :: list()) :: func_return()
  def create(project, arke, args) do
    persistence_fn = @persistence[:arke_postgres][:create]
    with %Unit{} = unit <- Unit.load(arke, args, :create),
         {:ok, unit} <- Validator.validate(unit, :create, project),
         {:ok, unit} <- ArkeManager.call_func(arke, :before_create, [arke, unit]),
         {:ok, unit} <- handle_group_call_func(arke, unit, :before_unit_create),
         {:ok, unit} <- handle_link_parameters_unit(arke, unit),
         {:ok, unit} <- persistence_fn.(project, unit),
         {:ok, unit} <- ArkeManager.call_func(arke, :on_create, [arke, unit]),
         {:ok, unit} <- handle_link_parameters(unit, %{}),
         {:ok, unit} <- handle_group_call_func(arke, unit, :on_unit_create),
         do: {:ok, unit},
         else: ({:error, errors} -> {:error, errors})
  end

  defp handle_link_parameters_unit(%{id: :arke_link} = _, unit), do: {:ok, unit}

  defp handle_link_parameters_unit(
         %{data: parameters} = arke,
         %{metadata: %{project: project}} = unit
       ) do


    {errors, link_units} =
      Enum.filter(ArkeManager.get_parameters(arke), fn p -> p.arke_id == :link end)
      |> Enum.reduce({[], []}, fn p, {errors, link_units} ->
        arke = ArkeManager.get(String.to_existing_atom(p.data.arke_or_group_id), project)

        case handle_create_on_link_parameters_unit(
               project,
               unit,
               p,
               arke,
               Unit.get_value(unit, p.id)
             ) do
          {:ok, parameter, %Unit{} = link_unit} -> {errors, [{parameter, link_unit} | link_units]}
          {:ok, parameter, link_unit} -> {errors, link_units}
          {:error, e} -> {[e | errors], link_units}
        end
      end)

    case length(errors) > 0 do
      true ->
        Enum.map(link_units, fn {p, u} ->
          delete(project, u)
        end)

        {:error, errors}

      false ->
        args =
          Enum.reduce(link_units, %{}, fn {p, u}, args ->
            Map.put(args, p.id, Atom.to_string(u.id))
          end)

        {:ok, Unit.update(unit, args)}
    end
  end

  defp handle_create_on_link_parameters_unit(project, unit, parameter, arke, value)
       when is_nil(value),
       do: {:ok, parameter, value}

  defp handle_create_on_link_parameters_unit(project, unit, parameter, arke, value)
       when is_binary(value),
       do: {:ok, parameter, value}

  defp handle_create_on_link_parameters_unit(project, unit, parameter, arke, value)
       when is_binary(value),
       do: {:ok, parameter, value}

  defp handle_create_on_link_parameters_unit(project, unit, parameter, arke, value)
       when is_map(value) do
    value = Map.put(value, :runtime_data, %{link: unit, link_parameter: parameter})

    case create(project, arke, value) do
      {:ok, unit} -> {:ok, parameter, unit}
      {:error, error} -> {:error, error}
    end
  end

  defp handle_create_on_link_parameters_unit(_, _, parameter, _, value),
    do: {:ok, parameter, value}

  defp handle_group_call_func(arke, unit, func) do
    GroupManager.get_groups_by_arke(arke)
    |> Enum.reduce_while(unit, fn group, new_unit ->
      with {:ok, new_unit} <- GroupManager.call_func(group, func, [arke, new_unit]),
           do: {:cont, new_unit},
           else: ({:error, errors} -> {:halt, {:error, errors}})
    end)
    |> check_group_manager_functions_errors()
  end

  defp check_group_manager_functions_errors({:error, errors} = _), do: {:error, errors}
  defp check_group_manager_functions_errors(unit), do: {:ok, unit}

  @doc """
  Function to update an element

  ## Parameters
    - project => :atom =>  identify the `Arke.Core.Project`
    - unit => %{arke_struct} =>  unit to update
    - args => [list]  => list of key: value to update

  ## Example
      iex> name = QueryManager.get_by(id: "name")
      ...> QueryManager.update(:default, name, [max_length: 20])

  ## Returns
      {:ok,  %Arke.Core.Unit{} }
      {:error, [msg]}

  """
  @spec update(Unit.t(), args :: list()) :: func_return()
  def update(%{arke_id: arke_id, metadata: %{project: project}, data: data} = current_unit, args) do
    persistence_fn = @persistence[:arke_postgres][:update]
    arke = ArkeManager.get(arke_id, project)

    with %Unit{} = unit <- Unit.update(current_unit, args),
         {:ok, unit} <- Validator.validate(unit, :update, project),
         {:ok, unit} <- ArkeManager.call_func(arke, :before_update, [arke, unit]),
         {:ok, unit} <- handle_link_parameters_unit(arke, unit),
         {:ok, unit} <- persistence_fn.(project, unit),
         {:ok, unit} <- ArkeManager.call_func(arke, :on_update, [arke, current_unit, unit]),
         {:ok, unit} <- handle_link_parameters(unit, data),
         do: {:ok, unit},
         else: ({:error, errors} -> {:error, errors})
  end

  @doc """
  Function to delete a given unit
  ## Parameters
    - project => :atom =>  identify the `Arke.Core.Project`
    - unit => %{arke_struct} => the unit to delete
  ## Example
      iex> element = Arke.QueryManager.get_by(id: "name")
      ...> Arke.QueryManager.delete(element)

  ## Returns
      {:ok, _}

  """
  @spec delete(project :: atom(), Unit.t()) :: {:ok, any()}
  def delete(project, %{arke_id: arke_id} = unit) do
    arke = ArkeManager.get(arke_id, project)
    persistence_fn = @persistence[:arke_postgres][:delete]

    with {:ok, unit} <- ArkeManager.call_func(arke, :before_delete, [arke, unit]),
         {:ok, nil} <- persistence_fn.(project, unit),
         {:ok, unit} <- handle_group_call_func(arke, unit, :on_unit_delete),
         {:ok, _unit} <- ArkeManager.call_func(arke, :on_delete, [arke, unit]),
         do: {:ok, nil},
         else: ({:error, errors} -> {:error, errors})
  end

  @doc """
  Function to get a single element identified by the opts. Use `Arke.QueryManager.filter_by` if more than one element is returned
  ## Parameters
    - opts => %{map} || [keyword: value] || key1: value1, key2: value2 => identify the element to get

  ## Example
      iex> Arke.QueryManager.get_by(id: "name")
  """
  @spec get_by(opts :: list()) :: Unit.t() | nil
  def get_by(opts \\ []), do: basic_query(opts) |> one

  @doc """
  Function to get all the element which match the given criteria
  ## Parameters
    - opts => %{map} || [keyword: value] || key1: value1, key2: value2 => identify the element to get
    - operator => :atom => refer to [operators](#module-operators)

  ## Example
      iex> Arke.QueryManager.filter_by(id: "name")

  ## Return
      [ Arke.Core.Unit{}, ...]
  """
  @spec filter_by(opts :: list()) :: [Unit.t()] | []
  def filter_by(opts \\ []), do: basic_query(opts) |> all
  defp basic_query(opts) when is_map(opts), do: Map.to_list(opts) |> basic_query

  defp basic_query(opts) do
    {project, opts} = Keyword.pop!(opts, :project)
    {arke, opts} = Keyword.pop(opts, :arke, nil)

    arke = get_arke(arke, project)

    query(project: project, arke: arke) |> where(opts)
  end

  defp get_arke(nil, _), do: nil

  defp get_arke(arke, project) when is_binary(arke),
    do: String.to_existing_atom(arke) |> get_arke(project)

  defp get_arke(arke, project) when is_atom(arke), do: ArkeManager.get(arke, project)
  defp get_arke(arke, _), do: arke

  defp get_group(group, project) when is_binary(group),
    do: String.to_existing_atom(group) |> get_group(project)

  defp get_group(group, project) when is_atom(group), do: GroupManager.get(group, project)
  defp get_group(group, _), do: group

  @doc """
  Add an `:and` logic to a query

  ## Parameter
    - query => refer to `query/1`
    - negate => boolean => used to figure out whether the condition is to be denied
    - filters => refer to `condition/3 | conditions/1`

  ## Example
      iex> query = QueryManager.query(arke: nil, project: :arke_system)
      ...> query = QueryManager.and_(query, false, QueryManager.conditions(parameter__eq: "value"))

  ## Return
      %Arke.Core.Query{}
  """
  @spec and_(query :: Query.t(), negate :: boolean(), filters :: list()) :: Query.t()
  def and_(query, negate, filters) when is_list(filters),
    do: Query.add_filter(query, :and, negate, parse_base_filters(query, filters))

  def and_(_query, _negate, filters), do: raise("filters must be a list")

  @doc """
  Add an `:or` logic to a query

  ## Parameter
    - query => refer to `query/1`
    - negate => boolean => used to figure out whether the condition is to be denied
    - filters => refer to `condition/3 | conditions/1`

  ## Example
      iex> query = QueryManager.query(arke: nil, project: :arke_system)
      ...> query = QueryManager.or_(query, false, QueryManager.conditions(parameter__eq: "value"))

  ## Return
      %Arke.Core.Query{}
  """
  @spec or_(query :: Query.t(), negate :: boolean(), filters :: list()) :: Query.t()
  def or_(query, negate, filters) when is_list(filters),
    do: Query.add_filter(query, :or, negate, parse_base_filters(query, filters))

  def or_(_query, _negate, filters), do: raise("filters must be a list")

  defp parse_base_filters(query, filters) do
    Enum.reduce(filters, [], fn f, new_filters ->
      parameter = get_parameter(query, f.parameter)
      [Query.new_base_filter(parameter, f.operator, f.value, f.negate) | new_filters]
    end)
  end

  @doc """
  Create a `Arke.Core.Query.BaseFilter`

  ## Parameters
    - parameter => :atom | %Arke.Core.Arke{} => the parameter where to check the condition
    - operator => :atom => refer to [operators](#module-operators)
    - value => string | boolean | nil => the value the parameter and operator must check
    - negate => boolean => used to figure out whether the condition is to be denied

  ## Example
      iex> QueryManager.condition(:string, :eq, "test")

  ## Return
      %Arke.Core.Query.BaseFilter{}
  """
  @spec condition(
          parameter :: Arke.t() | atom(),
          negate :: boolean(),
          value :: String.t() | boolean() | nil,
          negate :: boolean()
        ) :: Query.BaseFilter.t()
  def condition(parameter, operator, value, negate \\ false),
    do: Query.new_base_filter(parameter, operator, value, negate)

  @doc """
  Create a list of `Arke.Core.Query.BaseFilter`

  ## Parameter
    - opts => %{map} || [keyword: value] || key1: value1, key2: value2 => the condtions used to create the BaseFilters

  ## Example
      iex>  QueryManager.conditions(parameter__eq: "test", string__contains: "t")

  ## Return
      [ %Arke.Core.Query.BaseFilter{}, ...]
  """
  @spec conditions(opts :: list()) :: [Query.BaseFilter.t()]
  def conditions(opts \\ []) do
    Enum.reduce(opts, [], fn {key, value}, filters ->
      {parameter, operator} = get_parameter_operator(nil, String.split(Atom.to_string(key), "__"))
      [condition(parameter, operator, value) | filters]
    end)
  end

  @doc """
  Create query with specific options

  ##  Parameters
    - query => refer to `query/1`
    - opts => %{map} || [keyword: value] || key1: value1, key2: value2 => keyword list containing the filter to apply

  ## Example
      iex> query = Arke.QueryManager.query()
      ...> QueryManager.where(query, [id__contains: "me", id__contains: "n"])

  ## Return
      %Arke.Core.Query{ %Arke.Core.Query.Filter{ ... base_filters: %Arke.Core.Query.BaseFilter{ ... }}}

  """
  @spec where(query :: Query.t(), opts :: list()) :: Query.t()
  def where(query, opts \\ []) do
    Enum.reduce(opts, query, fn {key, value}, new_query ->
      {parameter, operator} =
        get_parameter_operator(query.arke, String.split(Atom.to_string(key), "__"))

      filter(new_query, parameter, operator, value)
    end)
  end

  @doc """
  Filter of the query

  ## Parameters
    - query => refer to `query/1`
    - filter => refer to `Arke.Core.Query.Filter`

  ## Example
      iex> query = Arke.QueryManager.Query.t
      ...> parameter = Arke.Boundary.ParameterManager.get(:id,:arke_system)
      ...> Arke.Core.Query.new_filter(parameter,:equal,"name",false)
      ...> Arke.Core.Query.filter(query, filter

  """
  @spec filter(query :: Query.t(), filter :: Query.Filter.t()) :: Query.t()
  def filter(query, filter), do: Query.add_filter(query, filter)

  @doc """
  Filter of the query

  ## Parameters
    - query => refer to `query/1`
    - parameter => %{arke_struct} => arke struct of the parameter
    - operator => :atom => refer to [operators](#module-operators)
    - value => string | boolean | nil => the value the parameter and operator must check
    - negate => boolean => used to figure out whether the condition is to be denied

  ## Example
      iex> query = Arke.QueryManager.query()
      ...> QueryManager.filter(query, Arke.Core.Query.new_filter(Arke.Boundary.ParameterManager.get(:id,:default),:equal,"name",false))

  ## Return
      %Arke.Core.Query{...}

  """
  @spec filter(
          query :: Query.t(),
          parameter :: Arke.t() | String.t() | atom(),
          operator :: operator(),
          value :: String.t() | boolean() | number(),
          negate :: boolean()
        ) :: Query.t()
  def filter(query, parameter, operator, value, negate \\ false),
    do: handle_filter(query, parameter, operator, value, negate)

  defp handle_filter(query, "group", :eq, value, negate),
    do: handle_filter(query, :group_id, :eq, value, negate)

  defp handle_filter(query, "group_id", :eq, value, negate),
    do: handle_filter(query, :group_id, :eq, value, negate)

  defp handle_filter(query, :group, :eq, value, negate),
    do: handle_filter(query, :group_id, :eq, value, negate)

  defp handle_filter(query, :group_id, :eq, value, negate) do
    %{id: id, metadata: %{project: group_project}} = group = get_group(value, query.project)
    # arke_list = GroupManager.get_arke_list(group)
    arke_list =
      Enum.map(GroupManager.get_link(id, group_project, :arke_list), fn a ->
        Atom.to_string(a.id)
      end)

    handle_filter_group(query, group, arke_list, negate)
  end

  defp handle_filter(query, parameter, operator, value, negate),
    do: Query.add_filter(query, get_parameter(query, parameter), operator, value, negate)

  defp handle_filter_group(query, group, arke_list, negate) when is_nil(group), do: query

  defp handle_filter_group(query, group, arke_list, negate),
    do: handle_filter(query, :arke_id, :in, arke_list, negate)

  @doc """
  Define a criteria to order the element returned from a query

  ## Parameter
    - query => refer to `query/1`
    - order => int => number of element to return

  ## Example
      iex> query = QueryManager.query()
      ...> parameter = Arke.Boundary.ParameterManager.get(:id,:default)
      ...> QueryManager.order(query, parameter, :asc)

  """
  @spec order(
          query :: Query.t(),
          parameter :: Arke.t() | String.t() | atom(),
          direction :: atom()
        ) :: Query.t()
  def order(query, parameter, direction),
    do: Query.add_order(query, get_parameter(query, parameter), direction)

  @doc """
  Set the offset of the  query

  ## Parameter
    - query => refer to `query/1`
    - offset => int => offset of the query

  ## Example
      iex> query = QueryManager.query()
      ...> QueryManager.where(query, id: "name") |> QueryManager.offset(5)

  """
  @spec offset(query :: Query.t(), offset :: integer()) :: Query.t()
  def offset(query, offset), do: Query.set_offset(query, offset)

  @doc """
  Set the limit of the element to be returned from a query

  ## Parameter
    - query => refer to `query/1`
    - limit => int => number of element to return

  ## Example
      iex> query = QueryManager..query()
      ...> QueryManager.where(query, id: "name") |> QueryManager.limit(1)

  """
  @spec limit(query :: Query.t(), limit :: integer()) :: Query.t()
  def limit(query, limit), do: Query.set_limit(query, limit)

  def pagination(query, offset, limit) do
    tmp_query = %{query | orders: []}
    count = count(tmp_query)
    elements = query |> offset(offset) |> limit(limit) |> all
    {count, elements}
  end

  @doc """
  Return all the results from a query

  ## Parameter
    - query => refer to `query/1`
  """
  @spec all(query :: Query.t()) :: [Unit.t()] | []
  def all(query), do: execute_query(query, :all)

  @doc """
  Return the first result of a query

  ## Parameter
    - query => refer to `query/1`
  """
  @spec one(query :: Query.t()) :: Unit.t() | nil
  def one(query), do: execute_query(query, :one)

  @doc """
  Return the query as a string

  ## Parameter
    - query => refer to `query/1`
  """
  @spec raw(query :: Query.t()) :: String.t()
  def raw(query), do: execute_query(query, :raw)

  @doc """
  Return the count of the element returned from a query

  ## Parameter
    - query => refer to `query/1`
  """
  @spec count(query :: Query.t()) :: integer()
  def count(query), do: execute_query(query, :count)

  @doc """
  Return a string which represent the query itself

  ## Parameter
    - query => refer to `query/1`

  ## Example
      iex> query = QueryManager.query()
      ...> QueryManager.where(query, id: "name") |> QueryManager.pseudo_query

  ## Return
      #Ecto.Query<>
  """
  @spec pseudo_query(query :: Query.t()) :: Ecto.Query.t()
  def pseudo_query(query), do: execute_query(query, :pseudo_query)

  ######################################################################################################################
  # PRIVATE FUNCTIONS ##################################################################################################
  ######################################################################################################################

  defp execute_query(query, action) do
    query = handle_arke_filter(query)
    persistence_fn = @persistence[:arke_postgres][:execute_query]
    persistence_fn.(query, action)
  end

  defp handle_arke_filter(%{arke: nil} = query), do: query

  defp handle_arke_filter(%{arke: %{data: %{type: "arke"}, id: id}} = query),
    do: filter(query, :arke_id, :eq, id)

  defp handle_arke_filter(query), do: query

  defp get_parameter_operator(arke, [key, operator]), do: {key, String.to_existing_atom(operator)}
  defp get_parameter_operator(arke, [key]), do: {key, :eq}
  # TODO custom exception
  defp get_parameter_operator(_, _), do: nil

  defp get_parameter(%{arke: nil, project: project} = query, %{id: id} = _parameter),
    do: ParameterManager.get(id, project)

  defp get_parameter(%{arke: nil, project: project} = query, key),
    do: ParameterManager.get(key, project)

  defp get_parameter(%{arke: arke, project: project} = query, key) do
    ArkeManager.get_parameter(arke, project, key)
  end

  defp handle_link_parameters(
         %{arke_id: arke_id, metadata: %{project: project}, data: new_data, id: id} = unit,
         old_data
       ) do
    arke = ArkeManager.get(arke_id, project)

    Enum.filter(ArkeManager.get_parameters(arke), fn p -> p.arke_id == :link end)
    |> Enum.each(fn p ->
      old_value = Map.get(old_data, p.id, nil)
      new_value = Map.get(new_data, p.id, nil)
      handle_link_parameter(unit, p, old_value, new_value)
    end)

    {:ok, unit}
  end

  defp handle_link_parameter(_, nil, _, _), do: nil

  defp handle_link_parameter(unit, %{data: %{multiple: false}} = parameter, old_value, new_value) do
    update_parameter_link(
      unit,
      parameter,
      normalize_value(old_value),
      :delete,
      old_value == new_value
    )

    update_parameter_link(
      unit,
      parameter,
      normalize_value(new_value),
      :add,
      old_value == new_value
    )

    {:ok, unit}
  end

  defp handle_link_parameter(unit, %{data: %{multiple: true}} = parameter, old_value, new_value) do
    # TODO make more efficient with bulk actions

    old_value = old_value || []
    new_value = new_value || []
    nodes_to_delete = Enum.map(old_value -- new_value, &normalize_value(&1))

    nodes_to_add = Enum.map(new_value -- old_value, &normalize_value(&1))

    Enum.each(nodes_to_delete, fn n ->
      update_parameter_link(unit, parameter, n, :delete, false)
    end)

    Enum.each(nodes_to_add, fn n ->
      IO.inspect({unit.id, parameter, n, :add, false})
      update_parameter_link(unit, parameter, n, :add, false)
    end)

    {:ok, unit}
  end

  defp update_parameter_link(_, _, _, _, true), do: nil
  defp update_parameter_link(_, _, nil, _, _), do: nil

  defp update_parameter_link(
         %{metadata: %{project: project}} = unit,
         %{
           id: p_id,
           data: %{connection_type: connection_type, direction: "child"}
         } = _parameter,
         id_to_link,
         action,
         false
       ) do
    handle_update_parameter_link(
      project,
      Atom.to_string(unit.id),
      id_to_link,
      connection_type,
      p_id,
      action
    )
  end

  defp update_parameter_link(
         %{metadata: %{project: project}} = unit,
         %{
           id: p_id,
           data: %{connection_type: connection_type, direction: "parent"}
         } = _parameter,
         id_to_link,
         action,
         false
       ) do
    handle_update_parameter_link(
      project,
      id_to_link,
      Atom.to_string(unit.id),
      connection_type,
      p_id,
      action
    )
  end

  defp handle_update_parameter_link(project, from, to, connection_type, p_id, :add) do
    LinkManager.add_node(project, from, to, connection_type, %{parameter_id: Atom.to_string(p_id)})
  end

  defp handle_update_parameter_link(project, from, to, connection_type, p_id, :delete) do
    LinkManager.delete_node(project, from, to, connection_type, %{
      parameter_id: Atom.to_string(p_id)
    })
  end

  # Function to get only the parameter id from `handle_link_parameter`
  defp normalize_value(nil), do: nil

  defp normalize_value(%{id: id} = value) do
    to_string(id)
  end

  defp normalize_value(value), do: to_string(value)
end