defmodule MvWeb.MemberLive.Index.FieldVisibility do
  @moduledoc """
  Manages field visibility by merging user-specific selection with global settings.

  This module handles:
  - Getting all available fields (member fields + custom fields)
  - Merging user selection with global settings (user selection takes priority)
  - Falling back to global settings when no user selection exists
  - Converting between different field name formats (atoms vs strings)

  ## Field Naming Convention

  - **Member Fields**: Atoms (e.g., `:first_name`, `:email`)
  - **Custom Fields**: Strings with format `"custom_field_<id>"` (e.g., `"custom_field_abc-123"`)

  ## Priority Order

  1. User-specific selection (from URL/Session/Cookie)
  2. Global settings (from database)
  3. Default (all fields visible)

  ## Pseudo Member Fields

  Overview-only fields that are not in `Mv.Constants.member_fields()` (e.g. computed/UI-only).
  They appear in the field dropdown and in `member_fields_visible` but are not domain attributes.
  """

  alias Mv.Membership.Helpers.VisibilityConfig

  # Single UI key for "Membership Fee Status"; only this appears in the dropdown.
  # Groups is also a pseudo field (not a DB attribute, but displayed in the table).
  @pseudo_member_fields [:membership_fee_status, :groups]

  # Export/API may accept this as alias; must not appear in the UI options list.
  @export_only_alias :payment_status

  defp overview_member_fields do
    Mv.Constants.member_fields() ++ @pseudo_member_fields
  end

  @doc """
  Gets all available fields for selection.

  Returns a list of field identifiers:
  - Member fields as atoms (e.g., `:first_name`, `:email`)
  - Custom fields as strings (e.g., `"custom_field_abc-123"`)

  ## Parameters

    - `custom_fields` - List of CustomField resources that are available

  ## Returns

    List of field identifiers (atoms and strings)
  """
  @spec get_all_available_fields([struct()]) :: [atom() | String.t()]
  def get_all_available_fields(custom_fields) do
    member_fields =
      overview_member_fields()
      |> Enum.reject(fn field -> field == @export_only_alias end)

    custom_field_names = Enum.map(custom_fields, &"custom_field_#{&1.id}")

    member_fields ++ custom_field_names
  end

  @doc """
  Merges user field selection with global settings.

  User selection takes priority over global settings. If a field is not in the
  user selection, the global setting is used. If a field is not in global settings,
  it defaults to `true` (visible).

  ## Parameters

    - `user_selection` - Map of field names (strings) to boolean visibility
    - `global_settings` - Settings struct with `member_field_visibility` field
    - `custom_fields` - List of CustomField resources

  ## Returns

    Map of field names (strings) to boolean visibility values

  ## Examples

      iex> user_selection = %{"first_name" => false}
      iex> settings = %{member_field_visibility: %{first_name: true, email: true}}
      iex> merge_with_global_settings(user_selection, settings, [])
      %{"first_name" => false, "email" => true}  # User selection overrides global
  """
  @spec merge_with_global_settings(
          %{String.t() => boolean()},
          map(),
          [struct()]
        ) :: %{String.t() => boolean()}
  def merge_with_global_settings(user_selection, global_settings, custom_fields) do
    all_fields = get_all_available_fields(custom_fields)
    global_visibility = get_global_visibility_map(global_settings, custom_fields)

    Enum.reduce(all_fields, %{}, fn field, acc ->
      field_string = field_to_string(field)

      visibility =
        case Map.get(user_selection, field_string) do
          nil -> Map.get(global_visibility, field_string, true)
          user_value -> user_value
        end

      Map.put(acc, field_string, visibility)
    end)
  end

  @doc """
  Gets the list of visible fields from a field selection map.

  Returns only fields where visibility is `true`.

  ## Parameters

    - `field_selection` - Map of field names to boolean visibility

  ## Returns

    List of field identifiers (atoms for member fields, strings for custom fields)

  ## Examples

      iex> selection = %{"first_name" => true, "email" => false, "street" => true}
      iex> get_visible_fields(selection)
      [:first_name, :street]
  """
  @spec get_visible_fields(%{String.t() => boolean()}) :: [atom() | String.t()]
  def get_visible_fields(field_selection) when is_map(field_selection) do
    field_selection
    |> Enum.filter(fn {_field, visible} -> visible end)
    |> Enum.map(fn {field_string, _visible} -> to_field_identifier(field_string) end)
    |> Enum.uniq()
  end

  def get_visible_fields(_), do: []

  @doc """
  Gets visible member fields from field selection.

  Returns only member fields (atoms) that are visible.

  ## Examples

      iex> selection = %{"first_name" => true, "email" => true, "custom_field_123" => true}
      iex> get_visible_member_fields(selection)
      [:first_name, :email]
  """
  @spec get_visible_member_fields(%{String.t() => boolean()}) :: [atom()]
  def get_visible_member_fields(field_selection) when is_map(field_selection) do
    member_fields = overview_member_fields()

    field_selection
    |> Enum.filter(fn {field_string, visible} ->
      field_atom = to_field_identifier(field_string)
      visible && field_atom in member_fields
    end)
    |> Enum.map(fn {field_string, _visible} -> to_field_identifier(field_string) end)
    |> Enum.uniq()
  end

  def get_visible_member_fields(_), do: []

  @doc """
  Returns the list of computed (UI-only) member field atoms.

  These fields are not in the database; they must not be used for Ash query
  select/sort. Use this to filter sort options and validate sort_field.
  """
  @spec computed_member_fields() :: [atom()]
  def computed_member_fields, do: @pseudo_member_fields

  @doc """
  Visible member fields that are real DB attributes (from `Mv.Constants.member_fields()`).

  Use for query select/sort. Not for rendering column visibility (use
  `get_visible_member_fields/1` for that).
  """
  @spec get_visible_member_fields_db(%{String.t() => boolean()}) :: [atom()]
  def get_visible_member_fields_db(field_selection) when is_map(field_selection) do
    db_fields = MapSet.new(Mv.Constants.member_fields())

    field_selection
    |> Enum.filter(fn {field_string, visible} ->
      field_atom = to_field_identifier(field_string)
      visible && field_atom in db_fields
    end)
    |> Enum.map(fn {field_string, _visible} -> to_field_identifier(field_string) end)
    |> Enum.uniq()
  end

  def get_visible_member_fields_db(_), do: []

  @doc """
  Visible member fields that are computed/UI-only (e.g. membership_fee_status).

  Use for rendering; do not use for query select or sort.
  """
  @spec get_visible_member_fields_computed(%{String.t() => boolean()}) :: [atom()]
  def get_visible_member_fields_computed(field_selection) when is_map(field_selection) do
    computed_set = MapSet.new([:membership_fee_status])

    field_selection
    |> Enum.filter(fn {field_string, visible} ->
      field_atom = to_field_identifier(field_string)
      visible && field_atom in computed_set
    end)
    |> Enum.map(fn {field_string, _visible} -> to_field_identifier(field_string) end)
    |> Enum.uniq()
  end

  def get_visible_member_fields_computed(_), do: []

  @doc """
  Gets visible custom fields from field selection.

  Returns only custom field identifiers (strings) that are visible.

  ## Examples

      iex> selection = %{"first_name" => true, "custom_field_123" => true, "custom_field_456" => false}
      iex> get_visible_custom_fields(selection)
      ["custom_field_123"]
  """
  @spec get_visible_custom_fields(%{String.t() => boolean()}) :: [String.t()]
  def get_visible_custom_fields(field_selection) when is_map(field_selection) do
    prefix = Mv.Constants.custom_field_prefix()

    field_selection
    |> Enum.filter(fn {field_string, visible} ->
      visible && String.starts_with?(field_string, prefix)
    end)
    |> Enum.map(fn {field_string, _visible} -> field_string end)
  end

  def get_visible_custom_fields(_), do: []

  # Gets global visibility map from settings
  defp get_global_visibility_map(settings, custom_fields) do
    member_visibility = get_member_field_visibility_from_settings(settings)
    custom_field_visibility = get_custom_field_visibility(custom_fields)

    Map.merge(member_visibility, custom_field_visibility)
  end

  # Gets member field visibility from settings (domain fields from settings, pseudo fields default true)
  defp get_member_field_visibility_from_settings(settings) do
    visibility_config =
      VisibilityConfig.normalize(Map.get(settings, :member_field_visibility, %{}))

    domain_fields = Mv.Constants.member_fields()

    domain_map =
      Enum.reduce(domain_fields, %{}, fn field, acc ->
        field_string = Atom.to_string(field)
        default_visibility = if field == :exit_date, do: false, else: true
        show_in_overview = Map.get(visibility_config, field, default_visibility)
        Map.put(acc, field_string, show_in_overview)
      end)

    Enum.reduce(@pseudo_member_fields, domain_map, fn field, acc ->
      Map.put(acc, Atom.to_string(field), true)
    end)
  end

  # Gets custom field visibility (all custom fields with show_in_overview=true are visible)
  defp get_custom_field_visibility(custom_fields) do
    prefix = Mv.Constants.custom_field_prefix()

    Enum.reduce(custom_fields, %{}, fn custom_field, acc ->
      field_string = "#{prefix}#{custom_field.id}"
      visible = Map.get(custom_field, :show_in_overview, true)
      Map.put(acc, field_string, visible)
    end)
  end

  # Converts field string to atom (for member fields) or keeps as string (for custom fields).
  # Maps export-only alias to canonical UI key so only one option controls the column.
  defp to_field_identifier(field_string) when is_binary(field_string) do
    if String.starts_with?(field_string, Mv.Constants.custom_field_prefix()) do
      field_string
    else
      atom =
        try do
          String.to_existing_atom(field_string)
        rescue
          ArgumentError -> field_string
        end

      if atom == @export_only_alias, do: :membership_fee_status, else: atom
    end
  end

  # Converts field identifier to string
  defp field_to_string(field) when is_atom(field), do: Atom.to_string(field)
  defp field_to_string(field) when is_binary(field), do: field
end
