lib/arke/struct_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.StructManager do
  @moduledoc """
    Module that provides function for getting Arke and Unit structs.
    It also provides functions for encoding and decoding Units.
  """

  alias Arke.Boundary.GroupManager
  alias Arke.Boundary.ArkeManager
  alias Arke.QueryManager
  alias Arke.DatetimeHandler, as: DatetimeHandler
  alias Arke.Core.{Unit, Arke}

  @type parameter :: %{
          default: String.t() | boolean() | atom() | map() | list() | nil,
          helper_text: String.t() | nil,
          id: String.t(),
          label: String.t(),
          required: boolean() | nil,
          type: String.t(),
          key: String.t() | boolean() | atom() | map() | list() | nil
        }

  @doc """
  Function that encodes a Unit or list of Unit

  ## Parameters
    - unit => [%{arke_struct}] | %{arke_struct} => unit or list of units that we want to encode
    - type => :json => desired encode type

  ## Example
      iex> units = QueryManager.filter_by(arke_id: id)
      ...> StructManager.encode(units, type: :json)

  """
  @spec encode(unit :: Unit.t(), format :: atom()) :: %{
          key: String.t() | number() | boolean() | atom()
        }
  def encode(unit, opts \\ [])

  def encode(unit, opts) do
    type = Keyword.get(opts, :type, :json)
    load_links = Keyword.get(opts, :load_links, false)
    handle_encode(unit, type, load_links, opts)
  end

  defp handle_encode(u, type, load_links, opts \\ [])

  defp handle_encode([], _, _, _), do: []
  defp handle_encode(nil, _, _, _), do: nil

  defp handle_encode(units, type, load_links, opts) when is_list(units) do
    # TODO handle multiple project encode
    %{metadata: %{project: project}} = List.first(units, nil)

    link_units = handle_load_link(units, project, load_links, opts)
    opts = Keyword.put(opts, :link_units, link_units)

    Enum.map(units, fn u ->
      handle_encode(u, type, load_links, opts)
    end)
  end

  defp handle_encode(
         %{id: id, arke_id: arke_id, metadata: %{project: project}} = unit,
         type,
         load_links,
         opts
       ) do
    {link_units, opts} = Keyword.pop(opts, :link_units, nil)

    link_units =
      if link_units == nil,
        do: handle_load_link(unit, project, load_links, opts),
        else: link_units

    opts = Keyword.put(opts, :link_units, link_units)

    arke = ArkeManager.get(arke_id, project)

    base_data = %{
      id: Atom.to_string(id),
      arke_id: Atom.to_string(arke_id),
      inserted_at: DatetimeHandler.parse_datetime(unit.inserted_at, true),
      updated_at: DatetimeHandler.parse_datetime(unit.updated_at, true),
      # to remove project Map.delete(unit.metadata, :project)
      metadata: unit.metadata
    }

    {:ok, new_unit} = ArkeManager.call_func(arke, :on_struct_encode, [arke, unit])

    data = get_parsed_data(new_unit.data, arke, opts) |> Map.merge(base_data)
    # TODO figure out why in link units project key in metadata is a string
    Map.put(data, :metadata, Map.drop(data.metadata, [:project, "project"]))
  end

  defp handle_load_link(_, _, false, _opts), do: []

  defp handle_load_link(unit, project, true, opts) do
    # IO.inspect("query done for link load")

    get_link_id_list(unit)
    |> get_link_units(project, opts)
  end

  # TODO forced load_links as false for link units
  defp get_link_units([], _project, _opts), do: []

  defp get_link_units(id_list, project, opts) do
    QueryManager.filter_by(id__in: id_list, project: project)
    |> Enum.map(fn unit -> encode(unit, Keyword.put(opts, :load_links, false)) end)
  end

  defp get_link_id_list(units) when is_list(units) do
    link_id_list =
      Enum.reduce(units, [], fn unit, acc ->
        acc ++ get_link_id_list(unit)
      end)
  end

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

    ArkeManager.get_parameters(arke)
    |> Enum.filter(fn param -> param.arke_id == :link end)
    |> Enum.reduce([], fn param, id_list ->
      link_id = Map.get(data, param.id, nil)
      updates_id_list(id_list, link_id)
    end)
  end

  defp updates_id_list(id_list, id) when is_nil(id), do: id_list
  defp updates_id_list(id_list, id) when is_list(id), do: id_list ++ id
  defp updates_id_list(id_list, id), do: [id | id_list]

  defp get_parsed_data(data, arke, opts \\ [])
  defp get_parsed_data(nil, arke, _), do: %{}
  defp get_parsed_data(data, arke, _) when data == %{}, do: %{}

  defp get_parsed_data(data, arke, opts) do
    data
    |> Enum.map(fn {k, v} -> validate_data(k, v, arke, opts) end)
    |> Enum.reduce(fn key, new_value ->
      Map.merge(key, new_value)
    end)
  end

  def encode(_unit, _format), do: raise("Must pass a valid unit")
  def validate_data(id, value, arke, opts \\ [])

  def validate_data(id, value, arke, opts) do
    param = ArkeManager.get_parameter(arke, id)
    new_value = parse_value(value, param, Enum.into(opts, %{}))
    %{id => new_value}
  end

  defp parse_value(value, param, _opts \\ [])

  defp parse_value(value, %{arke_id: :link} = _param, opts) do
    load_links = Map.get(opts, :load_links, false)

    Map.get(opts, :link_units, [])
    |> filter_link_units(value, load_links)
  end

  defp parse_value(value, %{data: %{values: nil}} = param, %{load_values: true} = opts) do
    opts = Map.delete(opts, :load_values)
    parse_value(value, param, opts)
  end

  defp parse_value(
         value,
         %{arke_id: param_type, data: %{multiple: false, values: values}} = param,
         %{load_values: true} = opts
       )
       when param_type in [:string, :float, :integer] and is_list(values) do
    Enum.find(values, fn map -> Map.get(map, :value, nil) == value end)
  end

  defp parse_value(
         value,
         %{arke_id: param_type, data: %{multiple: true, values: values}} = param,
         %{load_values: true} = opts
       )
       when param_type in [:string, :float, :integer] and is_list(values) and is_list(value) do
    Enum.reduce(value, [], fn v, new_value ->
      [Enum.find(values, fn map -> Map.get(map, :value, nil) == v end) | new_value]
    end)
  end

  defp parse_value(value, _param, _opts), do: value

  defp filter_link_units(link_units, map_list, false) when is_list(map_list) do
    Enum.map(map_list, fn map -> filter_link_units(link_units, map, false) end)
  end

  defp filter_link_units(_link_units, %{id: id, metadata: metadata} = unit, false)
       when is_atom(id),
       do: Atom.to_string(id)

  defp filter_link_units(_link_units, id, false), do: id

  defp filter_link_units(link_units, id_list, true) when is_list(id_list),
    do: Enum.filter(link_units, fn unit -> unit.id in id_list end)

  defp filter_link_units(link_units, id, true) do
    Enum.find(link_units, fn unit -> unit.id == id end)
  end

  @doc """
  Function that decodes data into a Unit or list of Unit

  ## Parameters
    - project => :atom =>  identify the `Arke.Core.Project`
    - arke_id => atom | string => arke id
    - json => %{key: value} => json data that we want to decode
    - type => :json => data input type

  ## Example
      iex> StructManager.decode(:arke, my_json_data, :json)
  """
  @spec decode(
          project :: atom(),
          arke_id :: atom(),
          json :: %{key: String.t() | number() | boolean() | atom()},
          format :: atom()
        ) :: Unit.t()
  def decode(project, arke_id, json, :json) when is_atom(arke_id) do
    ArkeManager.get(arke_id, project)
    |> Unit.load(json)
  end

  def decode(project, arke_id, json, :json) when is_binary(arke_id) do
    ArkeManager.get(String.to_existing_atom(arke_id), project)
    |> Unit.load(json)
  end

  def decode(_project, _arke_id, _json, _format), do: raise("Must pass valid data")

  def load(arke_id, data) do
  end

  defp handle_default_value(
         %{arke_id: :string, data: %{default_string: default_string}} = _,
         value
       )
       when is_nil(value),
       do: default_string

  defp handle_default_value(
         %{arke_id: :integer, data: %{default_integer: default_integer}} = _,
         value
       )
       when is_nil(value),
       do: default_integer

  defp handle_default_value(%{arke_id: :float, data: %{default_float: default_float}} = _, value)
       when is_nil(value),
       do: default_float

  defp handle_default_value(
         %{arke_id: :boolean, data: %{default_boolean: default_boolean}} = _,
         value
       )
       when is_nil(value),
       do: default_boolean

  defp handle_default_value(%{arke_id: :date, data: %{default_date: default_date}} = _, value)
       when is_nil(value),
       do: default_date

  defp handle_default_value(%{arke_id: :time, data: %{default_time: default_time}} = _, value)
       when is_nil(value),
       do: default_time

  defp handle_default_value(
         %{arke_id: :datetime, data: %{default_datetime: default_datetime}} = _,
         value
       )
       when is_nil(value),
       do: default_datetime

  defp handle_default_value(%{arke_id: :dict, data: %{default_dict: default_dict}} = _, value)
       when is_nil(value),
       do: default_dict

  defp handle_default_value(%{arke_id: :link, data: %{default_link: default_link}} = _, value)
       when is_nil(value),
       do: default_link

  defp handle_default_value(_, value), do: value

  @doc """
  Function that returns a Struct that describes an Arke or a Unit

  ## Parameters
    - unit => %Unit{} | %Arke{} => unit or arke struct

  ## Example
        iex> arke = ArkeManager.get(:test, :default)
        ...> StructManager.get_struct(arke)
  """
  @spec get_struct(arke :: Unit.t()) :: %{parameters: [parameter()], label: String.t()}
  def get_struct(%{arke_id: :arke, data: data} = arke) do
    struct = %{parameters: get_struct_parameters(arke, %{}), label: data.label}
    ArkeManager.call_func(arke, :after_get_struct, [arke, struct])
  end

  def get_struct(arke, %{data: data} = unit, opts) do
    struct = %{
      parameters: get_struct_parameters(arke, unit, opts),
      label: arke.data.label
    }
    ArkeManager.call_func(arke, :after_get_struct, [arke, unit, struct])
  end

  def get_struct(arke, %{data: data} = unit) do
    struct = %{
      parameters: get_struct_parameters(arke, unit, %{}),
      label: arke.data.label
    }
    ArkeManager.call_func(arke, :after_get_struct, [arke, unit, struct])
  end

  @spec get_struct(arke :: Unit.t(), opts :: list()) :: %{
          parameters: [parameter()],
          label: String.t()
        }
  def get_struct(%{arke_id: :arke, data: data} = arke, opts) do
    struct = %{
      parameters: get_struct_parameters(arke, opts),
      label: data.label
    }
    ArkeManager.call_func(arke, :after_get_struct, [arke, struct])
  end

  def get_struct(_), do: raise("Must pass a valid arke or unit")

  defp get_filtered_parameters(parameters, %{"exclude" => exclude}) do
    Enum.filter(parameters, fn p -> !(Atom.to_string(p.id) in exclude) end)
  end

  defp get_filtered_parameters(parameters, %{"include" => include}) do
    Enum.filter(parameters, fn p -> Atom.to_string(p.id) in include end)
  end

  defp get_filtered_parameters(parameters, _), do: parameters

  defp get_struct_parameters(%{metadata: %{project: project}} = arke, opts) do
    ArkeManager.get_parameters(arke)
    |> get_filtered_parameters(opts)
    |> Enum.reduce([], fn p, acc ->
      [base_parameter_struct(p) |> add_type_fields(project) | acc]
    end)
  end

  defp get_struct_parameters(%{metadata: %{project: project}} = arke, unit, opts) do
    ArkeManager.get_parameters(arke)
    |> get_filtered_parameters(opts)
    |> Enum.reduce([], fn p, acc ->
      [base_parameter_struct(p) |> add_type_fields(project) |> add_value(unit) | acc]
    end)
  end

  defp base_parameter_struct(%{id: id, arke_id: arke_id, data: data} = parameter) do
    {
      parameter,
      %{
        label: data.label,
        id: Atom.to_string(id),
        type: Atom.to_string(arke_id),
        required: data.required,
        helper_text: data.helper_text
      }
    }
  end

  defp add_value(%{id: "id"} = struct, %{id: id} = _), do: Map.merge(struct, %{value: id})

  defp add_value(%{id: "arke_id"} = struct, %{arke_id: arke_id} = _),
    do: Map.merge(struct, %{value: arke_id})

  defp add_value(%{id: "metadata"} = struct, %{metadata: metadata} = _),
    do: Map.merge(struct, %{value: Map.drop(metadata, [:project])})

  defp add_value(%{id: "inserted_at"} = struct, %{inserted_at: inserted_at} = _),
    do: Map.merge(struct, %{value: inserted_at})

  defp add_value(%{id: "updated_at"} = struct, %{updated_at: updated_at} = _),
    do: Map.merge(struct, %{value: updated_at})

  defp add_value(struct, %{data: data} = unit) do
    Map.merge(struct, %{
      value: Unit.get_value(data, String.to_existing_atom(struct.id))
    })
  end

  ######################################################################
  # STRING PARAMETER ###################################################
  ######################################################################

  defp add_type_fields({%{arke_id: :string, data: data} = parameter, base_data}, _project) do
    Map.merge(base_data, %{
      max_length: data.max_length,
      min_length: data.min_length,
      strip: data.strip,
      default: data.default_string,
      values: data.values,
      multiple: data.multiple
    })
  end

  ######################################################################
  # NUMBER PARAMETER ###################################################
  ######################################################################

  defp add_type_fields({%{arke_id: :integer, data: data} = _parameter, base_data}, _project) do
    Map.merge(base_data, %{
      max: data.max,
      min: data.min,
      default: data.default_integer,
      values: data.values,
      multiple: data.multiple
    })
  end

  defp add_type_fields({%{arke_id: :float, data: data} = _parameter, base_data}, _project) do
    Map.merge(base_data, %{
      max: data.max,
      min: data.min,
      default: data.default_float,
      values: data.values,
      multiple: data.multiple
    })
  end

  ######################################################################
  # BOOLEAN PARAMETER ##################################################
  ######################################################################

  defp add_type_fields({%{arke_id: :boolean, data: data} = parameter, base_data}, _project) do
    Map.merge(base_data, %{
      default: data.default_boolean
    })
  end

  ######################################################################
  # DICT PARAMETER #####################################################
  ######################################################################

  defp add_type_fields({%{arke_id: :dict, data: data} = parameter, base_data}, _project) do
    Map.merge(base_data, %{
      default: data.default_dict
    })
  end

  ######################################################################
  # LIST PARAMETER #####################################################
  ######################################################################

  defp add_type_fields({%{arke_id: :list, data: data} = parameter, base_data}, _project) do
    Map.merge(base_data, %{
      default: data.default_list
    })
  end

  ######################################################################
  # LINK PARAMETER #####################################################
  ######################################################################

  defp add_type_fields(
         {%{arke_id: :link, data: data} = _parameter, base_data},
         project
       ) do

    Map.merge(base_data, %{
      default: data.default_link,
      multiple: data.multiple,
      filter_keys: data.filter_keys,
      link_ref: encode(get_arke_or_group_id(data.arke_or_group_id, project), type: :json)
      # depth: data.depth,
      # connection_type: data.connection_type
    })
  end

  defp get_arke_or_group_id(nil, project), do: nil

  defp get_arke_or_group_id(arke_or_group_id, project) do
    case ArkeManager.get(arke_or_group_id, project) do
      {:error, _} ->
        case GroupManager.get(arke_or_group_id, project) do
          {:error, _} -> nil
          group -> group
        end
      nil ->
        case GroupManager.get(arke_or_group_id, project) do
          {:error, _} -> nil
          group -> group
        end
      arke ->
        arke
    end
  end

  ######################################################################
  # DEFAULT PARAMETER ##################################################
  ######################################################################
  defp add_type_fields({p, base_data}, _project) do
    base_data
  end
end