defmodule MvWeb.Components.MemberFilterComponent do
  @moduledoc """
  Provides the MemberFilter Live-Component.

  A DaisyUI dropdown filter for filtering members by payment status and boolean custom fields.
  Uses radio inputs in a segmented control pattern (join + btn) for tri-state boolean filters.

  ## Design Decisions

  - Uses `div` panel instead of `ul.menu/li` structure to avoid DaisyUI menu styles
    (padding, display, hover, font sizes) that would interfere with form controls.
  - Filter controls are form elements (fieldset with legend, radio inputs), not menu items.
    Uses semantic `<fieldset>` and `<legend>` for proper accessibility and form structure.
  - Dropdown stays open when clicking filter segments to allow multiple filter changes.
  - Uses `phx-change` on form for radio inputs instead of individual `phx-click` events.

  ## Props
  - `:cycle_status_filter` - Current payment filter state: `nil` (all), `:paid`, or `:unpaid`
  - `:groups` - List of groups (for per-group filter rows)
  - `:group_filters` - Map of active group filters: `%{group_id => :in | :not_in}` (nil = All for that group).
    Multiple active filters combine with AND (member must match all selected group conditions).
  - `:boolean_custom_fields` - List of boolean custom fields to display
  - `:boolean_filters` - Map of active boolean filters: `%{custom_field_id => true | false}`
  - `:id` - Component ID (required)
  - `:member_count` - Number of filtered members to display in badge (optional, default: 0)

  ## Events
  - Sends `{:payment_filter_changed, filter}` to parent when payment filter changes
  - Sends `{:group_filter_changed, group_id_str, value}` to parent when a group filter changes (value: nil | :in | :not_in)
  - Sends `{:boolean_filter_changed, custom_field_id, filter_value}` to parent when boolean filter changes
  """
  use MvWeb, :live_component

  @group_filter_prefix "group_"

  @impl true
  def mount(socket) do
    {:ok, assign(socket, :open, false)}
  end

  @impl true
  def update(assigns, socket) do
    socket =
      socket
      |> assign(:id, assigns.id)
      |> assign(:cycle_status_filter, assigns[:cycle_status_filter])
      |> assign(:groups, assigns[:groups] || [])
      |> assign(:group_filters, assigns[:group_filters] || %{})
      |> assign(:group_filter_prefix, @group_filter_prefix)
      |> assign(:boolean_custom_fields, assigns[:boolean_custom_fields] || [])
      |> assign(:boolean_filters, assigns[:boolean_filters] || %{})
      |> assign(:member_count, assigns[:member_count] || 0)

    {:ok, socket}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div
      class="relative member-filter-dropdown"
      id={@id}
      phx-click-away={if @open, do: "close_dropdown", else: nil}
      phx-window-keydown={@open && "close_dropdown"}
      phx-key="Escape"
      phx-target={@myself}
    >
      <.button
        type="button"
        variant="secondary"
        class={[
          "gap-2",
          (@cycle_status_filter || map_size(@group_filters) > 0 ||
             active_boolean_filters_count(@boolean_filters) > 0) &&
            "btn-active"
        ]}
        phx-click="toggle_dropdown"
        phx-target={@myself}
        aria-haspopup="true"
        aria-expanded={to_string(@open)}
        aria-label={gettext("Filter members")}
      >
        <.icon name="hero-funnel" class="h-5 w-5" />
        <span class="hidden sm:inline">
          {button_label(
            @cycle_status_filter,
            @groups,
            @group_filters,
            @boolean_custom_fields,
            @boolean_filters
          )}
        </span>
        <.badge
          :if={active_boolean_filters_count(@boolean_filters) > 0}
          variant="primary"
          size="sm"
        >
          {active_boolean_filters_count(@boolean_filters)}
        </.badge>
        <.badge
          :if={
            (@cycle_status_filter || map_size(@group_filters) > 0) &&
              active_boolean_filters_count(@boolean_filters) == 0
          }
          variant="primary"
          size="sm"
        >
          {@member_count}
        </.badge>
      </.button>

    <!--
        NOTE: We use a div panel instead of ul.menu/li structure to avoid DaisyUI menu styles
        (padding, display, hover, font sizes) that would interfere with our form controls.
        Filter controls are form elements (fieldset with legend, radio inputs), not menu items.
        We use semantic fieldset/legend structure for proper accessibility.
        We use relative/absolute positioning instead of DaisyUI dropdown classes to have
        full control over the open/close state via LiveView.
      -->
      <div
        :if={@open}
        tabindex="0"
        class="absolute left-0 mt-2 w-[28rem] rounded-box border border-base-300 bg-base-100 p-4 shadow-xl z-[100]"
        role="dialog"
        aria-label={gettext("Member filter")}
      >
        <form phx-change="update_filters" phx-target={@myself} data-testid="member-filter-form">
          <!-- Payment Filter Group -->
          <div class="mb-4">
            <div class="text-xs font-semibold opacity-70 mb-2 uppercase tracking-wider">
              {gettext("Payments")}
            </div>
            <fieldset class="grid grid-cols-[1fr_auto] items-center gap-3 py-2 border-0 p-0 m-0 min-w-0">
              <legend class="text-sm font-medium col-start-1 float-left w-auto">
                {gettext("Payment Status")}
              </legend>
              <div class="join col-start-2">
                <label
                  class={"#{payment_filter_label_class(@cycle_status_filter, nil)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
                  for="payment-filter-all"
                >
                  <input
                    type="radio"
                    id="payment-filter-all"
                    name="payment_filter"
                    value="all"
                    class="absolute opacity-0 w-0 h-0 pointer-events-none"
                    checked={@cycle_status_filter == nil}
                  />
                  <span class="text-xs">{gettext("All")}</span>
                </label>
                <label
                  class={"#{payment_filter_label_class(@cycle_status_filter, :paid)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
                  for="payment-filter-paid"
                >
                  <input
                    type="radio"
                    id="payment-filter-paid"
                    name="payment_filter"
                    value="paid"
                    class="absolute opacity-0 w-0 h-0 pointer-events-none"
                    checked={@cycle_status_filter == :paid}
                  />
                  <.icon name="hero-check-circle" class="h-5 w-5" />
                  <span class="text-xs">{gettext("Paid")}</span>
                </label>
                <label
                  class={"#{payment_filter_label_class(@cycle_status_filter, :unpaid)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
                  for="payment-filter-unpaid"
                >
                  <input
                    type="radio"
                    id="payment-filter-unpaid"
                    name="payment_filter"
                    value="unpaid"
                    class="absolute opacity-0 w-0 h-0 pointer-events-none"
                    checked={@cycle_status_filter == :unpaid}
                  />
                  <.icon name="hero-x-circle" class="h-5 w-5" />
                  <span class="text-xs">{gettext("Unpaid")}</span>
                </label>
              </div>
            </fieldset>
          </div>

    <!-- Groups: one row per group with All / Yes / No (like Custom Fields) -->
          <div :if={length(@groups) > 0} class="mb-4">
            <div class="text-xs font-semibold opacity-70 mb-2 uppercase tracking-wider">
              {gettext("Groups")}
            </div>
            <div class="max-h-60 overflow-y-auto pr-2">
              <fieldset
                :for={group <- @groups}
                class="grid grid-cols-[1fr_auto] items-center gap-3 py-2 border-b border-base-200 last:border-0 border-0 p-0 m-0 min-w-0"
              >
                <legend class="text-sm font-medium col-start-1 float-left w-auto">
                  {group.name}
                </legend>
                <div class="join col-start-2">
                  <label
                    class={"#{group_filter_label_class(@group_filters, group.id, nil)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
                    for={"group-filter-#{group.id}-all"}
                  >
                    <input
                      type="radio"
                      id={"group-filter-#{group.id}-all"}
                      name={"#{@group_filter_prefix}#{group.id}"}
                      value="all"
                      class="absolute opacity-0 w-0 h-0 pointer-events-none"
                      checked={Map.get(@group_filters, to_string(group.id)) == nil}
                    />
                    <span class="text-xs">{gettext("All")}</span>
                  </label>
                  <label
                    class={"#{group_filter_label_class(@group_filters, group.id, :in)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
                    for={"group-filter-#{group.id}-in"}
                    aria-label={gettext("Yes")}
                    title={gettext("Yes")}
                  >
                    <input
                      type="radio"
                      id={"group-filter-#{group.id}-in"}
                      name={"#{@group_filter_prefix}#{group.id}"}
                      value="in"
                      class="absolute opacity-0 w-0 h-0 pointer-events-none"
                      checked={Map.get(@group_filters, to_string(group.id)) == :in}
                    />
                    <.icon name="hero-check-circle" class="h-5 w-5" />
                    <span class="text-xs">{gettext("Yes")}</span>
                  </label>
                  <label
                    class={"#{group_filter_label_class(@group_filters, group.id, :not_in)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
                    for={"group-filter-#{group.id}-not-in"}
                    aria-label={gettext("No")}
                    title={gettext("No")}
                  >
                    <input
                      type="radio"
                      id={"group-filter-#{group.id}-not-in"}
                      name={"#{@group_filter_prefix}#{group.id}"}
                      value="not_in"
                      class="absolute opacity-0 w-0 h-0 pointer-events-none"
                      checked={Map.get(@group_filters, to_string(group.id)) == :not_in}
                    />
                    <.icon name="hero-x-circle" class="h-5 w-5" />
                    <span class="text-xs">{gettext("No")}</span>
                  </label>
                </div>
              </fieldset>
            </div>
          </div>

    <!-- Custom Fields Group -->
          <div :if={length(@boolean_custom_fields) > 0} class="mb-2">
            <div class="text-xs font-semibold opacity-70 mb-2 uppercase tracking-wider">
              {gettext("Individual datafields")}
            </div>
            <div class="max-h-60 overflow-y-auto pr-2">
              <fieldset
                :for={custom_field <- @boolean_custom_fields}
                class="grid grid-cols-[1fr_auto] items-center gap-3 py-2 border-b border-base-200 last:border-0 border-0 p-0 m-0 min-w-0"
              >
                <legend class="text-sm font-medium col-start-1 float-left w-auto">
                  {custom_field.name}
                </legend>
                <div class="join col-start-2">
                  <label
                    class={"#{boolean_filter_label_class(@boolean_filters, custom_field.id, nil)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
                    for={"custom-boolean-filter-#{custom_field.id}-all"}
                  >
                    <input
                      type="radio"
                      id={"custom-boolean-filter-#{custom_field.id}-all"}
                      name={"custom_boolean[#{custom_field.id}]"}
                      value="all"
                      class="absolute opacity-0 w-0 h-0 pointer-events-none"
                      checked={Map.get(@boolean_filters, to_string(custom_field.id)) == nil}
                    />
                    <span class="text-xs">{gettext("All")}</span>
                  </label>
                  <label
                    class={"#{boolean_filter_label_class(@boolean_filters, custom_field.id, true)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
                    for={"custom-boolean-filter-#{custom_field.id}-true"}
                    aria-label={gettext("Yes")}
                    title={gettext("Yes")}
                  >
                    <input
                      type="radio"
                      id={"custom-boolean-filter-#{custom_field.id}-true"}
                      name={"custom_boolean[#{custom_field.id}]"}
                      value="true"
                      class="absolute opacity-0 w-0 h-0 pointer-events-none"
                      checked={Map.get(@boolean_filters, to_string(custom_field.id)) == true}
                    />
                    <.icon name="hero-check-circle" class="h-5 w-5" />
                    <span class="text-xs">{gettext("Yes")}</span>
                  </label>
                  <label
                    class={"#{boolean_filter_label_class(@boolean_filters, custom_field.id, false)} has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary"}
                    for={"custom-boolean-filter-#{custom_field.id}-false"}
                    aria-label={gettext("No")}
                    title={gettext("No")}
                  >
                    <input
                      type="radio"
                      id={"custom-boolean-filter-#{custom_field.id}-false"}
                      name={"custom_boolean[#{custom_field.id}]"}
                      value="false"
                      class="absolute opacity-0 w-0 h-0 pointer-events-none"
                      checked={Map.get(@boolean_filters, to_string(custom_field.id)) == false}
                    />
                    <.icon name="hero-x-circle" class="h-5 w-5" />
                    <span class="text-xs">{gettext("No")}</span>
                  </label>
                </div>
              </fieldset>
            </div>
          </div>

    <!-- Footer -->
          <div class="mt-4 flex justify-between pt-3 border-t border-base-200">
            <.button
              type="button"
              variant="neutral"
              size="sm"
              phx-click="reset_filters"
              phx-target={@myself}
            >
              {gettext("Clear filters")}
            </.button>
            <.button
              type="button"
              variant="primary"
              size="sm"
              phx-click="close_dropdown"
              phx-target={@myself}
            >
              {gettext("Close")}
            </.button>
          </div>
        </form>
      </div>
    </div>
    """
  end

  @impl true
  def handle_event("toggle_dropdown", _params, socket) do
    {:noreply, assign(socket, :open, !socket.assigns.open)}
  end

  @impl true
  def handle_event("close_dropdown", _params, socket) do
    {:noreply, assign(socket, :open, false)}
  end

  @impl true
  def handle_event("update_filters", params, socket) do
    # Parse payment filter
    payment_filter =
      case Map.get(params, "payment_filter") do
        "paid" -> :paid
        "unpaid" -> :unpaid
        _ -> nil
      end

    # Parse per-group filters (params keys "group_<uuid>" => "all"|"in"|"not_in")
    prefix_len = String.length(@group_filter_prefix)

    group_filters_parsed =
      params
      |> Enum.filter(fn {key, _} -> String.starts_with?(key, @group_filter_prefix) end)
      |> Enum.reduce(%{}, fn {key, value_str}, acc ->
        group_id_str = String.slice(key, prefix_len, String.length(key) - prefix_len)
        filter_value = parse_group_filter_value(value_str)
        Map.put(acc, group_id_str, filter_value)
      end)

    # Parse boolean custom field filters (including nil values for "all")
    custom_boolean_filters_parsed =
      params
      |> Map.get("custom_boolean", %{})
      |> Enum.reduce(%{}, fn {custom_field_id_str, value_str}, acc ->
        filter_value = parse_tri_state(value_str)
        Map.put(acc, custom_field_id_str, filter_value)
      end)

    # Update payment filter if changed
    if payment_filter != socket.assigns.cycle_status_filter do
      send(self(), {:payment_filter_changed, payment_filter})
    end

    # Update group filters - send event for each changed group
    current_group_filters = socket.assigns.group_filters
    all_group_ids = MapSet.new(Enum.map(socket.assigns.groups, &to_string(&1.id)))

    Enum.each(group_filters_parsed, fn {group_id_str, new_value} ->
      in_set = MapSet.member?(all_group_ids, group_id_str)
      current_value = Map.get(current_group_filters, group_id_str)
      should_send = in_set and current_value != new_value

      if should_send do
        send(self(), {:group_filter_changed, group_id_str, new_value})
      end
    end)

    # Update boolean filters - send events for each changed filter
    current_filters = socket.assigns.boolean_filters

    # Process all custom field filters from form (including those set to "all"/nil)
    # Radio buttons in a group always send a value, so all active filters are in the form
    Enum.each(custom_boolean_filters_parsed, fn {custom_field_id_str, new_value} ->
      current_value = Map.get(current_filters, custom_field_id_str)

      # Only send event if value actually changed
      if current_value != new_value do
        send(self(), {:boolean_filter_changed, custom_field_id_str, new_value})
      end
    end)

    # Don't close dropdown - allow multiple filter changes
    {:noreply, socket}
  end

  @impl true
  def handle_event("reset_filters", _params, socket) do
    # Send single message to reset all filters at once (performance optimization)
    # This avoids N×2 load_members() calls when resetting multiple filters
    send(self(), {:reset_all_filters, nil, %{}, %{}})

    # Close dropdown after reset
    {:noreply, assign(socket, :open, false)}
  end

  # Parse tri-state filter value: "all" | "true" | "false" -> nil | true | false
  defp parse_tri_state("true"), do: true
  defp parse_tri_state("false"), do: false
  defp parse_tri_state("all"), do: nil
  defp parse_tri_state(_), do: nil

  defp parse_group_filter_value("in"), do: :in
  defp parse_group_filter_value("not_in"), do: :not_in
  defp parse_group_filter_value(_), do: nil

  # Get display label for button
  defp button_label(
         cycle_status_filter,
         groups,
         group_filters,
         boolean_custom_fields,
         boolean_filters
       ) do
    cond do
      cycle_status_filter ->
        payment_filter_label(cycle_status_filter)

      map_size(group_filters) > 0 ->
        group_filters_label(groups, group_filters)

      map_size(boolean_filters) > 0 ->
        boolean_filter_label(boolean_custom_fields, boolean_filters)

      true ->
        gettext("Apply filters")
    end
  end

  defp group_filters_label(_groups, group_filters) when map_size(group_filters) == 0,
    do: gettext("All")

  defp group_filters_label(groups, group_filters) do
    groups_by_id = Map.new(groups, fn g -> {to_string(g.id), g.name} end)

    names =
      group_filters
      |> Enum.map(fn {group_id_str, _} -> Map.get(groups_by_id, group_id_str) end)
      |> Enum.reject(&is_nil/1)

    label = Enum.join(names, ", ")
    truncate_label(label, 30)
  end

  # Get payment filter label
  defp payment_filter_label(nil), do: gettext("All")
  defp payment_filter_label(:paid), do: gettext("Paid")
  defp payment_filter_label(:unpaid), do: gettext("Unpaid")

  # Get boolean filter label (comma-separated list of active filter names)
  defp boolean_filter_label(_boolean_custom_fields, boolean_filters)
       when map_size(boolean_filters) == 0 do
    gettext("All")
  end

  defp boolean_filter_label(boolean_custom_fields, boolean_filters) do
    # Get names of active boolean filters
    active_filter_names =
      boolean_filters
      |> Enum.map(fn {custom_field_id_str, _value} ->
        Enum.find(boolean_custom_fields, fn cf -> to_string(cf.id) == custom_field_id_str end)
      end)
      |> Enum.filter(&(&1 != nil))
      |> Enum.map(& &1.name)

    # Join with comma and truncate if too long
    label = Enum.join(active_filter_names, ", ")
    truncate_label(label, 30)
  end

  # Truncate label if longer than max_length
  defp truncate_label(label, max_length) when byte_size(label) <= max_length, do: label

  defp truncate_label(label, max_length) do
    String.slice(label, 0, max_length) <> "..."
  end

  # Count active boolean filters
  defp active_boolean_filters_count(boolean_filters) do
    map_size(boolean_filters)
  end

  # Get CSS classes for payment filter label based on current state
  defp payment_filter_label_class(current_filter, expected_value) do
    base_classes = "join-item btn btn-sm"
    is_active = current_filter == expected_value

    cond do
      # All button (nil expected)
      expected_value == nil ->
        if is_active do
          "#{base_classes} btn-active"
        else
          "#{base_classes} btn"
        end

      # Paid button
      expected_value == :paid ->
        if is_active do
          "#{base_classes} btn-success btn-active"
        else
          "#{base_classes} btn"
        end

      # Unpaid button
      expected_value == :unpaid ->
        if is_active do
          "#{base_classes} btn-error btn-active"
        else
          "#{base_classes} btn"
        end

      true ->
        "#{base_classes} btn-outline"
    end
  end

  # Get CSS classes for per-group filter label based on current state
  defp group_filter_label_class(group_filters, group_id, expected_value) do
    base_classes = "join-item btn btn-sm"
    current_value = Map.get(group_filters, to_string(group_id))
    is_active = current_value == expected_value

    cond do
      expected_value == nil ->
        if is_active do
          "#{base_classes} btn-active"
        else
          "#{base_classes} btn"
        end

      expected_value == :in ->
        if is_active do
          "#{base_classes} btn-success btn-active"
        else
          "#{base_classes} btn"
        end

      expected_value == :not_in ->
        if is_active do
          "#{base_classes} btn-error btn-active"
        else
          "#{base_classes} btn"
        end

      true ->
        "#{base_classes} btn-outline"
    end
  end

  # Get CSS classes for boolean filter label based on current state
  defp boolean_filter_label_class(boolean_filters, custom_field_id, expected_value) do
    base_classes = "join-item btn btn-sm"
    current_value = Map.get(boolean_filters, to_string(custom_field_id))
    is_active = current_value == expected_value

    cond do
      # All button (nil expected)
      expected_value == nil ->
        if is_active do
          "#{base_classes} btn-active"
        else
          "#{base_classes} btn"
        end

      # True button
      expected_value == true ->
        if is_active do
          "#{base_classes} btn-success btn-active"
        else
          "#{base_classes} btn"
        end

      # False button
      expected_value == false ->
        if is_active do
          "#{base_classes} btn-error btn-active"
        else
          "#{base_classes} btn"
        end

      true ->
        "#{base_classes} btn-outline"
    end
  end
end
