defmodule ExTeal.Fields.ManyToMany do
  @moduledoc """
  The `ManyToMany` field corresponds to a `many_to_many` ecto relationship.  For example,
  let's assume a `User` schema has a `many_to_many` relationship with a `Role` schema.
  We can add the relationship to our `User` resource like so:

      alias ExTeal.Fields.ManyToMany


  ## Customizing Index Tables

  Let's assume we have a `User` schema with a `many_to_many` relationship with a `Role` schema.  The pivot
  schema has a :primary boolean field.  We want to display the `Role`'s name and the `User`'s email and
  the primary boolean field on the index table of 'Roles' for a 'User'. We can do this by defining:

      ManyToMany.make(:roles, Role)
      |> ManyToMany.with_pivot_fields([ExTeal.Fields.Boolean.make(:primary)])

  The relationship table, attach and edit-attached interfaces now all a user to manage the 'primary' field
  which only exists on the pivot table.

  ## Customizing Index Tables

  By default, the ManyToMany field produces a `ManyToManyBelongsTo` field on the index table.  This field
  appears as a BelongsTo field, providing the title for the linked resource and a link to the detail page
  of the linked asset, in the previous example 'Role'.  There are times where you might need to customize
  the index table, overriding the `ManyToManyBelongsTo` and providing a unique list of fields to display.

  Enter `with_index_fields/2` and `with_index_query/2`.  By default, Teal queries for both the related resource
  and the join to the current resource via some of the meta data associated with the underlying `Ecto.Assoc`
  struct.  The results are then loaded in a custom resource lookup select query.  When extending the default
  many-to-many functionality, the query must return a `ExTeal.Resource.pivot_resource()` type.

  ### Example

  Assume a `User` has many to many with `Org` through a `Membership` schema.  `Membership` has a role field and we want to display some
  extra information in the `Org` many-to-many table for a user.  We could define that as:

      ManyToMany.make(:orgs, Org)
      |> ManyToMany.with_index_fields([
        ExTeal.Fields.Number.make(:member_count) |> ExTeal.Field.virtual()
      |> ManyToMany.with_index_query(fn query, _assoc, _resource_id ->
        |> Ecto.Query.join(:left, [o], m in assoc(o, :memberships))
        |>, [o, x, m], %{
          _row: %{
            org_type: o.org_type,
            member_count: selected_as(count(, :member_count)
          _pivot: x,
          pivot: true
        |> Ecto.Query.group_by([o, x], [, x.org_id])
      |> ManyToMany.with_pivot_fields([ExTeal.Fields.Select.make(:role)])

  What a query, what a pipe. There are some conditions here.  This functionality is only available when
  a `many_to_many` assocation is defined with a join_through schema.  If the many to many is joined
  only through the table name, you may be able to customize the index fields by defining a `with_index_fields/2`
  and ignoring the `with_index_query/2` function.  If you need to customize the query, you can do so by
  simply returning a full schema and avoiding the `ExTeal.Resource.pivot_resource()` type.

  use ExTeal.Field
  alias ExTeal.Field
  alias ExTeal.Fields.ManyToManyBelongsTo
  alias ExTeal.Resource

  def make(relationship_name, module, label \\ nil) do
    field = Field.struct_from_field(__MODULE__, relationship_name, label)
    %{field | options: Map.merge(field.options, %{has_pivot_fields: false}), relationship: module}

  def component, do: "many-to-many"

  def value_for(_field, _model, _type), do: nil

  def show_on_index, do: false
  def show_on_new, do: false
  def show_on_edit, do: false

  def apply_options_for(field, model, conn, _type) do
    rel = model.__struct__.__schema__(:association, field.field)

    with {:ok, resource} <- ExTeal.resource_for_model(rel.queryable) do
      opts =
        Map.merge(field.options, %{
          many_to_many_relationship: field.field,
          listable: true

        | options: Map.merge(Resource.to_json(resource, conn), opts),
          private_options: Map.merge(field.private_options, %{rel: rel})

  def with_pivot_fields(field, pivot_fields) do
    pivot_fields =, &Map.put(&1, :pivot_field, true))

      | private_options: Map.merge(field.private_options, %{pivot_fields: pivot_fields}),
        options: Map.merge(field.options, %{has_pivot_fields: true})

  @spec with_index_fields(Field.t(), [Field.t()]) :: Field.t()
  def with_index_fields(many_to_many_field, index_fields) do
      | private_options:
          Map.merge(many_to_many_field.private_options, %{index_fields: index_fields})

  @spec with_index_query(Field.t(), (Ecto.Query.t(), struct(), any() -> Ecto.Query.t())) ::
  def with_index_query(many_to_many_field, index_query_fn) do
      | private_options:
          Map.merge(many_to_many_field.private_options, %{index_query_fn: index_query_fn})

  def sortable_by(field, pivot_field_name) do
    %{field | options: Map.merge(field.options, %{sortable_by: pivot_field_name})}

  def index_fields(queried_resource, rel_name, related_resource) do
    field_name = String.to_existing_atom(rel_name)
    field = Enum.find(queried_resource.fields(), &(&1.field == field_name))
    index_fields_for_many_to_many_field(field, field_name, related_resource, queried_resource)

  defp index_fields_for_many_to_many_field(
         %Field{private_options: %{index_fields: index_fields}},
       do: index_fields

  defp index_fields_for_many_to_many_field(_, field_name, related_resource, queried_resource) do
      ManyToManyBelongsTo.make(field_name, related_resource, queried_resource)