defmodule Carve.Links do
  @moduledoc """
  Carve.Links provides functionality for retrieving and processing links between different entities.

  This module is responsible for:
  - Fetching links for entities based on their IDs or data structures
  - Handling circular references and preventing infinite loops
  - Normalizing and preparing the final result set of links

  It works in conjunction with the view modules created using Carve.View to generate
  a complete set of links for any given entity or set of entities.
  """

  require Logger

  @doc """
  Retrieves links for a given module and ID or list of IDs.

  This function handles single IDs, lists of IDs, and prevents circular references
  using a visited map. It also supports filtering links based on a whitelist.

  ## Parameters

  - `module`: The module to use for fetching and processing links
  - `id`: A single ID or a list of IDs
  - `visited`: A map to keep track of visited entities (default: %{})
  - `whitelist`: An optional list of allowed link types (default: nil)

  ## Returns

  A list of prepared link maps.

  ## Examples

  iex> Carve.Links.get_links_by_id(UserJSON, 1)
  [%{type: :team, id: "abc123", data: %{...}}, %{type: :profile, id: "def456", data: %{...}}]

  iex> Carve.Links.get_links_by_id(UserJSON, [1, 2, 3], %{}, [:team])
  [%{type: :team, id: "abc123", data: %{...}}]
  """
  def get_links_by_id(module, id, visited \\ %{}, whitelist \\ nil)
  def get_links_by_id(_module, nil, _visited, _whitelist), do: []

  def get_links_by_id(module, id, visited, whitelist) when not is_list(id) do
    case Map.get(visited, {module, id}) do
      nil ->
        case module.get_by_id(id) do
          nil -> []
          data -> get_links_by_data(module, data, visited, whitelist) |> prepare_result(whitelist)
        end

      _ ->
        []
    end
  end

  def get_links_by_id(module, ids, visited, whitelist) when is_list(ids) do
    Enum.flat_map(ids, &get_links_by_id(module, &1, visited, whitelist))
    |> prepare_result(whitelist)
  end

  @doc """
  Retrieves links for a given module and data or list of data.

  This function handles single data structures, lists of data structures, and
  prevents circular references using a visited map. It also supports filtering links based on a whitelist.

  ## Parameters

  - `module`: The module to use for fetching and processing links
  - `data`: A single data structure or a list of data structures
  - `visited`: A map to keep track of visited entities (default: %{})
  - `whitelist`: An optional list of allowed link types (default: nil)

  ## Returns

  A list of prepared link maps.

  ## Examples

  iex> user = %{id: 1, name: "John Doe", team_id: 2}
  iex> Carve.Links.get_links_by_data(UserJSON, user)
  [%{type: :team, id: "abc123", data: %{...}}, %{type: :profile, id: "def456", data: %{...}}]

  iex> user = %{id: 1, name: "John Doe", team_id: 2}
  iex> Carve.Links.get_links_by_data(UserJSON, user, %{}, [:team])
  [%{type: :team, id: "abc123", data: %{...}}]
  """

  # Get links for an entity by its data, with support for collections and filtering
  def get_links_by_data(module, data, visited \\ %{}, whitelist \\ nil)

  # Return empty list for nil data
  def get_links_by_data(_module, nil, _visited, _whitelist), do: []

  # Handle collections by mapping over each item
  def get_links_by_data(module, data_list, visited, whitelist) when is_list(data_list) do
    Enum.flat_map(data_list, &get_links_by_data(module, &1, visited, whitelist))
    |> prepare_result(whitelist)
  end

  # Return empty list for non-map data
  def get_links_by_data(_module, data, _visited, _whitelist) when not is_map(data), do: []

  # Process single entity's links, with circular reference prevention
  def get_links_by_data(module, data, visited, whitelist) when not is_list(data) do
    case fetch_id(data) do
      {:ok, id} ->
        if Map.get(visited, {module, id}) do
          []
        else
          # Mark as visited and process links
          visited = Map.put(visited, {module, id}, true)

          # Get raw links and apply whitelist filtering/evaluation
          links =
            module.declare_links(data)
            |> filter_and_evaluate_links(whitelist)

          links =
            links
            |> Enum.flat_map(fn {link_module, link_ids} ->
              link_ids
              |> normalize_link_ids()
              |> Enum.map(fn link_id ->
	      link = process_single_link(link_module, link_id, visited)
	      children = if Map.get(visited, {link_module, link_id}) do
		[]
	      else
		 if is_map(link_id) do
		   get_links_by_data(link_module, link_id, visited, whitelist)
		 else
		   get_links_by_id(link_module, link_id, visited, whitelist)
		 end
	      end

              [link | children]
              end)
          end)

          prepare_result(links, whitelist)
        end

      :error ->
        []
    end
  end

  defp filter_and_evaluate_links(links, nil) do
    # Without whitelist - evaluate regular links only (no lazy)
    links
    |> Enum.filter(fn {_, value} -> not is_function(value) end)
    |> Enum.into(%{})
  end

  defp filter_and_evaluate_links(_links, []) do
    # Empty whitelist - evaluate nothing
    %{}
  end

  defp filter_and_evaluate_links(links, whitelist) when is_list(whitelist) do
    links
    |> Enum.filter(fn {module, _} -> module.type_name() in whitelist end)
    |> Enum.map(fn {module, value} ->
      evaluated =
        case value do
          fun when is_function(fun, 0) -> fun.()
          other -> other
        end

      {module, evaluated}
    end)
    |> Enum.into(%{})
  end

  @doc """
  Filters links based on the provided whitelist.

  ## Parameters

  - `links`: A map of links where keys are modules and values are IDs or lists of IDs
  - `whitelist`: A list of allowed link types (atoms)

  ## Returns

  A filtered map of links.

  ## Examples

  iex> links = %{UserJSON => [1, 2], PostJSON => [3, 4]}
  iex> Carve.Links.filter_links(links, [:user])
  %{UserJSON => [1, 2]}
  """
  defp filter_links(links, nil), do: links

  defp filter_links(links, whitelist) do
    links
    |> Enum.filter(fn {module, _} -> module.type_name() in whitelist end)
    |> Enum.into(%{})
  end

  @doc """
  Prepares the final result set by flattening, removing nil values, eliminating duplicates,
  and applying the whitelist filter if provided.

  ## Parameters

  - `result`: A list of link maps
  - `whitelist`: An optional list of allowed link types

  ## Returns

  A cleaned, uniquified, and optionally filtered list of link maps.

  ## Examples

  iex> result = [[%{type: :team, id: "abc"}, nil], [%{type: :profile, id: "def"}], %{type: :team, id: "abc"}]
  iex> Carve.Links.prepare_result(result, [:team])
  [%{type: :team, id: "abc"}]
  """
  def prepare_result(result, whitelist \\ nil) do
    result
    |> List.flatten()
    |> Enum.reject(&is_nil/1)
    |> Enum.uniq_by(fn %{type: type, id: id} -> {type, id} end)
    |> filter_result(whitelist)
  end

  defp filter_result(result, []), do: []
  defp filter_result(result, nil), do: result

  defp filter_result(result, whitelist) do
    Enum.filter(result, fn %{type: type} -> type in whitelist end)
  end

  @doc """
  Processes a single link, fetching its data and preparing it for output.

  This function handles both ID-based and data-based links, preventing circular
  references using the visited map.

  ## Parameters

  - `module`: The module to use for fetching and processing the link
  - `id_or_data`: Either an ID or a data structure representing the linked entity
  - `visited`: A map to keep track of visited entities

  ## Returns

  A prepared link map or nil if the link has already been visited or is invalid.
  """
  defp process_single_link(module, id, visited) when is_number(id) or is_binary(id) do
    case Map.get(visited, {module, id}) do
      nil ->
        case module.get_by_id(id) do
          nil ->
            nil

          data ->
            module.prepare_for_view(data)
        end

      _ ->
        nil
    end
  end

  defp process_single_link(module, data, visited) do
    case fetch_id(data) do
      {:ok, id} ->
        case Map.get(visited, {module, id}) do
          nil ->
            module.prepare_for_view(data)

          _ ->
            nil
        end

      :error ->
        nil
    end
  end

  @doc """
  Normalizes link IDs to always be a list.

  ## Parameters

  - `link_ids`: A single link ID or a list of link IDs

  ## Returns

  A list of link IDs.

  ## Examples

  iex> Carve.Links.normalize_link_ids(1)
  [1]

  iex> Carve.Links.normalize_link_ids([1, 2, 3])
  [1, 2, 3]
  """
  defp normalize_link_ids(link_ids) when is_list(link_ids), do: link_ids
  defp normalize_link_ids(link_id), do: [link_id]

  @doc """
  Fetches the ID from a data structure.

  This function attempts to find an ID in a map using various common key formats.

  ## Parameters

  - `data`: A map or other data structure potentially containing an ID

  ## Returns

  `{:ok, id}` if an ID is found, `:error` otherwise.

  ## Examples

  iex> Carve.Links.fetch_id(%{id: 1})
  {:ok, 1}

  iex> Carve.Links.fetch_id(%{"id" => 2})
  {:ok, 2}

  iex> Carve.Links.fetch_id(%{foo: "bar"})
  {:ok, {:foo, "bar"}}

  iex> Carve.Links.fetch_id("not a map")
  :error
  """
  defp fetch_id(data) when is_map(data) do
    cond do
      Map.has_key?(data, :id) ->
        {:ok, data.id}

      Map.has_key?(data, "id") ->
        {:ok, data["id"]}

      true ->
        case Enum.at(data, 0) do
          {key, value} -> {:ok, {key, value}}
          nil -> :error
        end
    end
  end

  defp fetch_id(id) when is_integer(id) or is_binary(id), do: {:ok, id}
  defp fetch_id(_), do: :error
end
