Current section

Files

Jump to
selecto_components lib selecto_components views aggregate component.ex
Raw

lib/selecto_components/views/aggregate/component.ex

defmodule SelectoComponents.Views.Aggregate.Component do
  @doc """
    display results of aggregate view
  """
  use Phoenix.LiveComponent
  alias SelectoComponents.ErrorHandling.ErrorBuilder
  alias SelectoComponents.Param
  alias SelectoComponents.Presentation
  alias SelectoComponents.QueryResults
  alias SelectoComponents.Theme
  alias SelectoComponents.Views.Aggregate.Options

  @impl true
  def mount(socket) do
    {:ok,
     assign(socket,
       aggregate_page: 0,
       aggregate_page_loading?: false,
       aggregate_requested_page: nil,
       theme: Theme.default_theme(:light)
     )}
  end

  @impl true
  def update(assigns, socket) do
    previous_exe_id = get_in(socket.assigns, [:view_meta, :exe_id])
    incoming_exe_id = get_in(assigns, [:view_meta, :exe_id])
    incoming_server_paged? = get_in(assigns, [:view_meta, :aggregate_server_paged?]) == true

    incoming_page =
      assigns
      |> Map.get(:aggregate_page, get_in(assigns, [:view_meta, :aggregate_page]))
      |> normalize_page()

    aggregate_page =
      if previous_exe_id && incoming_exe_id && previous_exe_id != incoming_exe_id do
        incoming_page
      else
        Map.get(socket.assigns, :aggregate_page, incoming_page)
      end

    # Force a complete re-assignment to ensure LiveView recognizes data changes
    socket = assign(socket, assigns)

    # Add a timestamp to force re-rendering if data changed
    socket =
      assign(socket,
        aggregate_page: aggregate_page,
        aggregate_server_paged?: incoming_server_paged?,
        aggregate_page_loading?: false,
        aggregate_requested_page: nil,
        presentation_context: Map.get(assigns, :presentation_context, %{}),
        theme:
          Map.get(assigns, :theme, Map.get(socket.assigns, :theme, Theme.default_theme(:light))),
        last_update: System.system_time(:microsecond)
      )

    {:ok, socket}
  end

  # Determine the hierarchy level of a ROLLUP result row
  # With COALESCE, NULL values appear as "[NULL]" strings
  # ROLLUP NULLs remain as nil or empty string
  defp rollup_level(row, num_group_by_cols) do
    group_cols = Enum.take(row, num_group_by_cols)

    non_nil_count =
      Enum.count(group_cols, fn col ->
        # Count as filled if:
        # - Not nil
        # - Not empty string (ROLLUP NULL)
        # - Not "[NULL]" string (but this IS a filled value from COALESCE - data NULL)
        not is_nil(col) and col != ""
      end)

    non_nil_count
  end

  # Prepare ROLLUP results with hierarchy metadata
  # With COALESCE, data NULLs show as "[NULL]", ROLLUP NULLs show as nil/empty
  # Filter out redundant [NULL] rows that are identical to their rollup subtotal.
  # Also mark which level-0 row is the true grand total so data NULL groups
  # can still render as clickable [NULL] buckets.
  defp prepare_rollup_rows(results, num_group_by_cols) do
    rows_with_metadata =
      results
      |> Enum.with_index()
      |> Enum.map(fn {row, idx} ->
        level = rollup_level(row, num_group_by_cols)
        group_cols = Enum.take(row, num_group_by_cols)

        # Check if this row has [NULL] at its current level (data NULL from LEFT JOIN)
        # Level 0 = grand total (no [NULL] possible)
        # Level N = first N columns filled, so check position N-1 for [NULL]
        has_null_at_level = level > 0 && Enum.at(group_cols, level - 1) == "[NULL]"

        {level, row, has_null_at_level, idx}
      end)

    # Filter out [NULL] rows if the next row (rollup subtotal) has identical aggregates
    filtered_rows =
      rows_with_metadata
      |> Enum.with_index()
      |> Enum.filter(fn {{level, row, has_null_at_level, _orig_idx}, current_idx} ->
        if has_null_at_level do
          # This is a row ending with [NULL] - check if next row is its rollup with same values
          next_row = Enum.at(rows_with_metadata, current_idx + 1)
          group_cols = Enum.take(row, num_group_by_cols)

          case next_row do
            {next_level, next_row_data, _has_null, _next_idx} when next_level == level - 1 ->
              # Next row is at the right level - verify it's OUR rollup by checking group columns
              current_group_prefix = Enum.take(group_cols, level - 1)
              next_group_cols = Enum.take(next_row_data, num_group_by_cols)
              next_group_prefix = Enum.take(next_group_cols, level - 1)

              if current_group_prefix == next_group_prefix do
                # Same group - this is our rollup subtotal, compare aggregates
                current_aggs = Enum.drop(row, num_group_by_cols)
                next_aggs = Enum.drop(next_row_data, num_group_by_cols)

                # If aggregates are identical, skip this [NULL] row (redundant)
                not (current_aggs == next_aggs)
              else
                # Different group - this must be end of our group, skip [NULL] row (redundant)
                false
              end

            _ ->
              # Next row is not at the right level or doesn't exist - skip [NULL] row
              false
          end
        else
          # Not a [NULL] row, always keep
          true
        end
      end)

    last_level0_idx =
      filtered_rows
      |> Enum.with_index()
      |> Enum.reduce(nil, fn
        {{{0, row, _has_null, _orig_idx}, _current_idx}, idx}, acc ->
          group_cols = Enum.take(row, num_group_by_cols)
          if Enum.all?(group_cols, &(&1 in [nil, ""])), do: idx, else: acc

        _other, acc ->
          acc
      end)

    filtered_rows
    |> Enum.with_index()
    |> Enum.map(fn {{{level, row, _has_null, _orig_idx}, _current_idx}, idx} ->
      grand_total? = idx == last_level0_idx
      {level, row, grand_total?}
    end)
  end

  # Format a value for display
  # With COALESCE, "[NULL]" strings are already in the data and should be displayed as-is
  # ROLLUP NULLs (nil/empty) should also be shown as "[NULL]"
  defp format_value(value, column_def \\ nil, presentation_context \\ %{}) do
    case value do
      nil ->
        "[NULL]"

      # Empty string from ROLLUP NULL
      "" ->
        "[NULL]"

      {coefficient, scale} when is_integer(coefficient) and is_integer(scale) and scale >= 0 ->
        safe_cell_value(
          format_decimal_tuple(coefficient, scale),
          column_def,
          presentation_context
        )

      {display_value, _id} when is_nil(display_value) or display_value == "" ->
        "[NULL]"

      {display_value, _id} ->
        safe_cell_value(display_value, column_def, presentation_context)

      tuple when is_tuple(tuple) ->
        elem_val = elem(tuple, 0)

        if is_nil(elem_val) or elem_val == "" do
          "[NULL]"
        else
          safe_cell_value(elem_val, column_def, presentation_context)
        end

      # Includes "[NULL]" strings from COALESCE
      _ ->
        safe_cell_value(value, column_def, presentation_context)
    end
  end

  defp safe_cell_value(value, column_def, presentation_context) do
    case value do
      {:safe, _} = safe_value ->
        safe_value

      nil ->
        ""

      {coefficient, scale}
      when is_integer(coefficient) and is_integer(scale) and scale >= 0 ->
        format_decimal_tuple(coefficient, scale)

      value when is_atom(value) ->
        Atom.to_string(value)

      value when is_tuple(value) ->
        inspect(value)

      _ ->
        value
        |> Presentation.format_cell(maybe_normalized_column(column_def), presentation_context)
        |> QueryResults.normalize_value()
    end
  end

  # Format an aggregate value, applying format function if present
  defp format_aggregate_value(value, coldef, presentation_context) do
    formatted =
      case coldef do
        %{format: fmt_fun} when is_function(fmt_fun) -> fmt_fun.(value)
        _ -> value
      end

    format_value(formatted, coldef, presentation_context)
  end

  defp format_decimal_tuple(coefficient, 0), do: Integer.to_string(coefficient)

  defp format_decimal_tuple(coefficient, scale) when coefficient < 0 do
    "-" <> format_decimal_tuple(abs(coefficient), scale)
  end

  defp format_decimal_tuple(coefficient, scale) do
    digits = Integer.to_string(coefficient)

    if String.length(digits) <= scale do
      "0." <> String.duplicate("0", scale - String.length(digits)) <> digits
    else
      split_at = String.length(digits) - scale
      {whole, fractional} = String.split_at(digits, split_at)
      whole <> "." <> fractional
    end
  end

  # Build filter attributes for drill-down from group column values
  # Now includes special handling for NULL values - uses "__NULL__" marker for IS_EMPTY filter
  # Uses indexed phx-value attributes to support multiple filter levels
  defp build_filter_attrs(group_cols, group_by_defs, level) do
    group_cols
    |> Enum.zip(group_by_defs)
    |> Enum.with_index()
    |> Enum.filter(fn {{_value, _def}, idx} ->
      # Include all values (including nil) up to current level
      idx < level
    end)
    |> Enum.reduce(%{}, fn {{value, {_alias, {:group_by, field, coldef}}}, idx}, acc ->
      put_filter_attr(acc, value, field, coldef, idx)
    end)
  end

  defp build_axis_filter_attrs(%{defs: defs, start_idx: start_idx}, value)
       when is_list(defs) and is_integer(start_idx) do
    values = if is_list(value), do: value, else: [value]

    values
    |> Enum.zip(defs)
    |> Enum.with_index(start_idx)
    |> Enum.reduce(%{}, fn {{axis_value, {_alias, {:group_by, field, coldef}}}, idx}, acc ->
      put_filter_attr(acc, axis_value, field, coldef, idx)
    end)
  end

  defp build_axis_filter_attrs(_axis, _value), do: %{}

  defp put_filter_attr(acc, value, field, coldef, idx) do
    filter_field = filter_field_for_group(field, coldef)
    filter_value = filter_value_for_group(value, field, coldef)

    acc
    |> Map.put("phx-value-field#{idx}", filter_field)
    |> Map.put("phx-value-value#{idx}", to_string(filter_value))
    |> Map.put("phx-value-gidx#{idx}", to_string(idx))
  end

  defp filter_field_for_group(field, coldef) do
    if legacy_tag_id_filter?(coldef) do
      id_filter_field(
        field,
        Map.get(coldef, :id_field) || Map.get(coldef, "id_field"),
        &to_string/1
      )
    else
      case coldef do
        %{group_by_filter: filter} when not is_nil(filter) ->
          filter

        %{"group_by_filter" => filter} when not is_nil(filter) ->
          filter

        %{join_mode: mode, id_field: id_field}
        when mode in [:lookup, :star, :tag] and not is_nil(id_field) ->
          id_filter_field(field, id_field, &Atom.to_string/1)

        %{"join_mode" => mode, "id_field" => id_field}
        when mode in ["lookup", "star", "tag"] and not is_nil(id_field) ->
          id_filter_field(field, id_field, &to_string/1)

        _ ->
          field_name_for_group(field)
      end
    end
  end

  defp legacy_tag_id_filter?(coldef) when is_map(coldef) do
    mode = Map.get(coldef, :join_mode) || Map.get(coldef, "join_mode")
    id_field = Map.get(coldef, :id_field) || Map.get(coldef, "id_field")
    filter = Map.get(coldef, :group_by_filter) || Map.get(coldef, "group_by_filter")

    group_by_filter_select =
      Map.get(coldef, :group_by_filter_select) ||
        Map.get(coldef, "group_by_filter_select")

    mode in [:tag, "tag"] and is_nil(group_by_filter_select) and not is_nil(id_field) and
      to_string(filter) == to_string(id_field)
  end

  defp legacy_tag_id_filter?(_coldef), do: false

  defp id_filter_field(field, id_field, fallback_fun) do
    case group_table_prefix(field) do
      nil -> fallback_fun.(id_field)
      table_prefix -> "#{table_prefix}.#{id_field}"
    end
  end

  defp group_table_prefix(field) do
    case field do
      {:row, [display_field | _], _} ->
        case display_field do
          {:coalesce, [inner | _]} -> extract_table_prefix(inner)
          _ -> extract_table_prefix(display_field)
        end

      {:field, field_ref, _} ->
        extract_table_prefix(field_ref)

      _ ->
        nil
    end
  end

  defp field_name_for_group(field) do
    case field do
      {:field, {:coalesce, [inner_field | _]}, _} ->
        case inner_field do
          {:to_char, {field_name, _format}} -> Atom.to_string(field_name)
          field_id when is_atom(field_id) -> Atom.to_string(field_id)
          field_id when is_binary(field_id) -> field_id
          _ -> "id"
        end

      {:field, {:to_char, {field_name, _format}}, _} ->
        Atom.to_string(field_name)

      {:field, field_id, _} when is_atom(field_id) ->
        Atom.to_string(field_id)

      {:field, field_id, _} when is_binary(field_id) ->
        field_id

      _ ->
        "id"
    end
  end

  # Extract table prefix from a field reference
  # Examples: "category.category_name" -> "category", :category_name -> nil
  defp extract_table_prefix(field_ref) do
    case field_ref do
      field_str when is_binary(field_str) ->
        case String.split(field_str, ".") do
          [table, _field] -> table
          _ -> nil
        end

      _ ->
        nil
    end
  end

  # Render a single row with hierarchy styling
  defp rollup_row(assigns) do
    continued? = Map.get(assigns, :continued?, false)
    grand_total? = Map.get(assigns, :grand_total?, false)
    row_index = Map.get(assigns, :row_index, 0)

    display_level =
      if assigns.level == 0 and not grand_total? and assigns.num_group_by > 0 do
        1
      else
        assigns.level
      end

    # Extract group columns and aggregate columns from the row
    group_cols = Enum.take(assigns.row, assigns.num_group_by)
    agg_cols = Enum.drop(assigns.row, assigns.num_group_by)

    # Determine styling based on hierarchy level
    {row_style, font_weight, indent_px} =
      case display_level do
        # Grand total
        0 ->
          {"background: color-mix(in srgb, var(--sc-accent-soft) 55%, var(--sc-surface-bg)); border-top: 2px solid var(--sc-accent);",
           "font-bold", 0}

        # Level 1 subtotal
        1 ->
          {"background: var(--sc-surface-bg-alt); border-top: 1px solid var(--sc-surface-border);",
           "font-semibold", 16}

        # Level 2 (or detail if only 2 levels)
        2 ->
          {"background: var(--sc-surface-bg); border-top: 1px solid var(--sc-surface-border);",
           "font-normal", 32}

        # Level 3
        3 ->
          {"background: var(--sc-surface-bg); border-top: 1px solid var(--sc-surface-border);",
           "font-normal", 48}

        # Deeper levels
        _ ->
          {"background: var(--sc-surface-bg); border-top: 1px solid var(--sc-surface-border);",
           "font-normal", 64}
      end

    # The maximum level is the number of group-by columns
    # If we're at max level, it's a detail row (not a subtotal)
    is_detail = display_level == assigns.num_group_by

    # For detail rows, use normal styling unless row is a continuation marker
    {row_style, font_weight} =
      cond do
        continued? ->
          {"background: color-mix(in srgb, var(--sc-accent-soft) 35%, var(--sc-surface-bg)); border-top: 1px dashed var(--sc-accent);",
           "font-semibold italic"}

        is_detail ->
          {"background: var(--sc-surface-bg); border-top: 1px solid var(--sc-surface-border);",
           "font-normal"}

        true ->
          {row_style, font_weight}
      end

    # Build filter attributes for drill-down (accumulated from all parent levels)
    filter_attrs = build_filter_attrs(group_cols, assigns.group_by, display_level)

    assigns =
      assign(assigns,
        group_cols: group_cols,
        agg_cols: agg_cols,
        row_style: row_style,
        font_weight: font_weight,
        indent_px: indent_px,
        row_index: row_index,
        filter_attrs: filter_attrs,
        continued?: continued?,
        display_level: display_level,
        grand_total?: grand_total?,
        active_group_range: active_group_range_for_level(assigns.group_by, display_level),
        group_blocks: row_group_blocks(assigns.group_by, group_cols)
      )

    ~H"""
    <tr style={@row_style} data-selecto-result-row data-result-row-index={@row_index}>
      <%!-- Render visible group-by blocks --%>
      <%= for block <- @group_blocks do %>
        <td
          class={"px-3 py-2 text-sm #{@font_weight}"}
          style="color: var(--sc-text-primary); border-bottom: 1px solid var(--sc-surface-border);"
          data-selecto-result-cell
          data-result-row-index={@row_index}
          data-result-column-index={block.start_idx}
          tabindex="-1"
        >
          <div style={"padding-left: #{group_block_padding(block, @active_group_range, @indent_px)}px"}>
            <%= if @grand_total? and @display_level == 0 and block.start_idx == 0 do %>
              <%!-- Grand total row --%>
              <span style="color: var(--sc-text-muted); font-style: italic;">Total</span>
            <% else %>
              <%= if display_group_block?(block, @active_group_range) do %>
                <%= if @continued? do %>
                  <span style="color: var(--sc-accent);">
                    {group_block_value(block, @presentation_context)} (continued)
                  </span>
                <% else %>
                  <div
                    phx-click="agg_add_filters"
                    {@filter_attrs}
                    data-selecto-result-action
                    class="cursor-pointer hover:underline"
                  >
                    {group_block_value(block, @presentation_context)}
                  </div>
                <% end %>
              <% end %>
            <% end %>
          </div>
        </td>
      <% end %>

      <%!-- Render aggregate columns --%>
      <%= for {{value, {_alias, {:agg, _agg, coldef}}}, agg_idx} <- Enum.with_index(Enum.zip(@agg_cols, @aggregate)) do %>
        <td
          class={"px-3 py-2 text-sm #{@font_weight}"}
          style="color: var(--sc-text-primary); border-bottom: 1px solid var(--sc-surface-border);"
          data-selecto-result-cell
          data-result-row-index={@row_index}
          data-result-column-index={@num_group_by + agg_idx}
          tabindex="-1"
        >
          <%= if @continued? do %>
            <span style="color: var(--sc-accent);">-</span>
          <% else %>
            {format_aggregate_value(value, coldef, @presentation_context)}
          <% end %>
        </td>
      <% end %>
    </tr>
    """
  end

  defp prepend_continued_group_headers(rows, num_group_by, aggregate_count, row_offset)
       when is_list(rows) do
    rendered_rows =
      Enum.map(rows, fn {level, row, grand_total?} -> {level, row, false, grand_total?} end)

    if row_offset > 0 do
      case rows do
        [{level, row, _grand_total?} | _] when level > 1 ->
          group_cols = Enum.take(row, num_group_by)

          continued_rows =
            1..(level - 1)
            |> Enum.map(fn parent_level ->
              parent_group_cols =
                Enum.take(group_cols, parent_level) ++
                  List.duplicate(nil, max(num_group_by - parent_level, 0))

              parent_agg_cols = List.duplicate(nil, aggregate_count)

              {parent_level, parent_group_cols ++ parent_agg_cols, true, false}
            end)

          continued_rows ++ rendered_rows

        _ ->
          rendered_rows
      end
    else
      rendered_rows
    end
  end

  defp prepend_continued_group_headers(rows, _num_group_by, _aggregate_count, _row_offset),
    do: rows

  @impl true
  def render(assigns) do
    # Check for execution error first
    if execution_error?(assigns) do
      error_info = ErrorBuilder.normalize(assigns.execution_error)
      assigns = assign(assigns, :error_info, error_info)

      ~H"""
      <div>
        <div class="mb-4 rounded px-4 py-3" role="alert" style="background: color-mix(in srgb, var(--sc-danger) 10%, var(--sc-surface-bg)); border: 1px solid color-mix(in srgb, var(--sc-danger) 35%, var(--sc-surface-border)); color: var(--sc-danger);">
          <strong class="font-bold">{@error_info.summary}:</strong>
          <span :if={present?(@error_info.user_message)} class="block sm:inline">
            {@error_info.user_message}
          </span>
          <span
            :if={not present?(@error_info.user_message)}
            class="block sm:inline font-mono text-xs"
          >
            {inspect(assigns.execution_error, pretty: true, limit: 12)}
          </span>
          <div :if={@error_info.detail} class="text-sm mt-1">{@error_info.detail}</div>
          <div :if={@error_info.suggestion} class="text-sm mt-1 font-medium">
            Next step: {@error_info.suggestion}
          </div>
        </div>
        <div class="p-4 italic" style="color: var(--sc-text-secondary);">
          Aggregate view cannot be displayed until the query error is resolved.
        </div>
      </div>
      """
    else
      # Check if we have valid query results and execution state
      case {assigns[:executed], assigns.query_results} do
        {false, _} ->
          # Query is being executed or hasn't been executed yet
          ~H"""
          <div>
            <div class="p-4 italic" style="color: var(--sc-accent);">Loading view...</div>
          </div>
          """

        {true, nil} ->
          # Executed but no results - this is an error state
          ~H"""
            <div>
              <div class="p-4" style="color: var(--sc-danger);">
                <div class="font-semibold">No Results</div>
                <div class="mt-1 text-sm">Query executed but returned no results.</div>
              </div>
            </div>
          """

        {true, {results, _fields, aliases}} ->
          # Valid execution with results - proceed with normal rendering

          # Extract the actual selected fields from the selecto configuration
          # Note: assigns.selecto.set.group_by contains ROLLUP config, not actual fields
          # The actual fields are in assigns.selecto.set.selected
          selected_fields = Map.get(assigns.selecto.set, :selected, []) || []

          # Also get the original group_by and aggregates for processing
          rollup_group_by = Map.get(assigns.selecto.set, :group_by, []) || []
          aggregates = Map.get(assigns.selecto.set, :aggregates, []) || []

          # Use the rollup rendering logic instead of simple flat rendering
          render_aggregate_view(
            assigns,
            results,
            aliases,
            selected_fields,
            rollup_group_by,
            aggregates
          )

        _ ->
          # Fallback for unexpected states
          ~H"""
          <div>
            <div class="p-4" style="color: var(--sc-text-secondary);">
              <div class="font-semibold">Unknown State</div>
              <div class="text-sm mt-1">
                Executed: {inspect(assigns[:executed])}<br />
                Query Results: {inspect(assigns.query_results != nil)}
              </div>
            </div>
          </div>
          """
      end
    end
  end

  defp render_aggregate_view(
         assigns,
         results,
         aliases,
         selected_fields,
         rollup_group_by,
         aggregates
       ) do
    # Use the actual selected fields for counting instead of group_by + aggregates
    # because ROLLUP can add extra fields to the query result
    expected_field_count = Enum.count(selected_fields)
    aliases_count = Enum.count(aliases)

    # If still mismatched at render time, check if we should show loading or error state
    if aliases_count != expected_field_count do
      # If we have no query results or they're stale, show loading
      # If executed is false, we're waiting for a new query
      cond do
        not assigns[:executed] ->
          ~H"""
          <div>
            <div class="p-4 italic" style="color: var(--sc-accent);">Loading view...</div>
          </div>
          """

        assigns.query_results == nil ->
          ~H"""
          <div>
            <div class="p-4 italic" style="color: var(--sc-accent);">Loading view...</div>
          </div>
          """

        true ->
          # We have results but they don't match - this suggests a configuration issue
          assigns =
            assign(assigns,
              expected_field_count: expected_field_count,
              aliases_count: aliases_count,
              selected_fields_count: Enum.count(selected_fields),
              aggregates_count: Enum.count(aggregates),
              aliases_debug: inspect(aliases)
            )

          ~H"""
          <div>
            <div class="p-4" style="color: var(--sc-danger);">
              <div class="font-semibold">View Configuration Error</div>
              <div class="text-sm mt-1">
                Expected {@expected_field_count} fields but got {@aliases_count} from query.
                This usually indicates a mismatch between the view configuration and query results.
              </div>
              <details class="mt-2 text-xs">
                <summary class="cursor-pointer">Debug Info</summary>
                <div>Selected Fields: {@selected_fields_count}</div>
                <div>Aggregate Fields: {@aggregates_count}</div>
                <div>Query Aliases: {@aliases_debug}</div>
              </details>
            </div>
          </div>
          """
      end
    else
      render_synchronized_view(
        assigns,
        results,
        aliases,
        selected_fields,
        rollup_group_by,
        aggregates
      )
    end
  end

  defp render_synchronized_view(
         assigns,
         results,
         aliases,
         selected_fields,
         rollup_group_by,
         aggregates
       ) do
    # Process the selected fields to match the aliases
    # The selected fields should match 1:1 with the aliases from the query
    field_mappings = Enum.zip(aliases, selected_fields)

    # Split the mappings back into group_by and aggregate sections
    # We need to determine which selected fields are group by vs aggregates
    # Look at the rollup_group_by to determine how many group by fields we have

    # Count the actual group by fields (not the ROLLUP wrapper)
    num_group_by =
      case Map.get(assigns.selecto.set, :groups, []) do
        groups when is_list(groups) and groups != [] ->
          length(groups)

        _ ->
          case rollup_group_by do
            [{:rollup, positions}] when is_list(positions) -> Enum.count(positions)
            _ -> 0
          end
      end

    # num_aggregates = Enum.count(selected_fields) - num_group_by

    group_by_mappings = Enum.take(field_mappings, num_group_by)
    aggregate_mappings = Enum.drop(field_mappings, num_group_by)

    selecto_group_by_config =
      Map.get(assigns.selecto.set, :gb_params) || Map.get(assigns.selecto.set, "gb_params")

    view_config_group_by =
      case assigns do
        %{view_config: view_config} when is_map(view_config) ->
          Map.get(view_config, :group_by) || Map.get(view_config, "group_by")

        _ ->
          nil
      end

    group_by_config = selecto_group_by_config || view_config_group_by || %{}

    group_by_param_configs =
      group_by_config
      |> Map.values()
      |> Enum.sort_by(fn config ->
        Param.integer(Map.get(config, "index", Map.get(config, :index)))
      end)

    group_by_param_fields =
      Enum.map(group_by_param_configs, fn cfg -> Map.get(cfg, "field") || Map.get(cfg, :field) end)

    # Convert to the format expected by the template
    group_by =
      group_by_mappings
      |> Enum.with_index()
      |> Enum.map(fn {{query_alias, field}, idx} ->
        display_alias = selected_field_alias(field, query_alias)

        # Get the proper column definition from selecto
        # Now that Selecto.field returns full definitions, we get all properties
        coldef =
          case field do
            {:field, {:to_char, {field_name, _format}}, _alias} ->
              # Handle formatted date fields
              configured_column(assigns.selecto, field_name) ||
                Selecto.field(assigns.selecto, field_name) || %{name: display_alias, format: nil}

            {:field, field_id, _alias} when is_binary(field_id) or is_atom(field_id) ->
              # Selecto.field now returns full custom column definitions with group_by_filter
              result =
                configured_column(assigns.selecto, field_id) ||
                  Selecto.field(assigns.selecto, field_id)

              if result == nil do
                # Field not found - use basic definition
                %{name: display_alias, format: nil}
              else
                result
              end

            {:field, {_extract_type, field_id, _format}, _alias} ->
              # Handle extracted fields (e.g., date parts)
              configured_column(assigns.selecto, field_id) ||
                Selecto.field(assigns.selecto, field_id) || %{name: display_alias}

            {:row, [display_field | _rest], _alias} ->
              # For row selectors (e.g., join mode columns), look up the actual column definition
              # display_field might be wrapped in COALESCE - extract the original field name
              field_name =
                case display_field do
                  {:coalesce, [inner_field | _]} -> inner_field
                  other -> other
                end

              # Look up metadata from domain.schemas for joined fields
              result =
                if is_binary(field_name) && String.contains?(field_name, ".") do
                  [schema_name, field_only] = String.split(field_name, ".", parts: 2)

                  SelectoComponents.SchemaUtils.lookup_domain_column_metadata(
                    assigns.selecto,
                    schema_name,
                    field_only
                  )
                else
                  configured_column(assigns.selecto, field_name) ||
                    Selecto.field(assigns.selecto, field_name)
                end

              if result == nil do
                # Field not found - use basic definition
                %{name: display_alias, format: nil}
              else
                result
              end

            _ ->
              # Fallback to basic definition
              %{name: display_alias, format: nil}
          end

        coldef = maybe_set_group_by_filter(coldef, Enum.at(group_by_param_fields, idx))
        coldef = maybe_set_group_by_format(coldef, Enum.at(group_by_param_configs, idx))
        {display_alias, {:group_by, field, coldef}}
      end)

    aggregates_processed =
      Enum.zip(aggregate_mappings, aggregates)
      |> Enum.map(fn {{query_alias, selected_field}, agg} ->
        display_alias = selected_field_alias(selected_field, query_alias)

        # Get the proper column definition from selecto
        coldef =
          case agg do
            {:field, {_func, field_id}, _alias} when is_atom(field_id) ->
              configured_column(assigns.selecto, field_id) ||
                Selecto.field(assigns.selecto, field_id)

            {:field, field_id, _alias} when is_atom(field_id) ->
              configured_column(assigns.selecto, field_id) ||
                Selecto.field(assigns.selecto, field_id)

            {:field, {_func, field_id}, _alias} when is_binary(field_id) ->
              configured_column(assigns.selecto, field_id) ||
                Selecto.field(assigns.selecto, field_id)

            {:field, field_id, _alias} when is_binary(field_id) ->
              configured_column(assigns.selecto, field_id) ||
                Selecto.field(assigns.selecto, field_id)

            _ ->
              # Fallback to empty map for unknown aggregate types
              %{}
          end

        {display_alias, {:agg, agg, coldef}}
      end)

    # Prepare rollup rows with hierarchy level metadata
    num_group_by = length(group_by)
    rollup_rows = prepare_rollup_rows(results, num_group_by)

    page_row_count = length(rollup_rows)
    aggregate_meta = Map.get(assigns, :view_meta, %{})
    server_paged? = Map.get(aggregate_meta, :aggregate_server_paged?, false)

    total_rows_before_cap =
      Map.get(aggregate_meta, :aggregate_total_rows_before_cap, page_row_count)

    rows_capped? = Map.get(aggregate_meta, :aggregate_rows_capped?, false)

    max_client_rows =
      Map.get(aggregate_meta, :aggregate_max_client_rows, Options.default_max_client_rows())

    per_page_setting =
      assigns
      |> Map.get(:view_meta, %{})
      |> Map.get(:per_page, Options.default_per_page())
      |> Options.normalize_per_page_param()

    per_page = Options.per_page_to_int(per_page_setting, page_row_count)

    {paged_rollup_rows, aggregate_total_rows, current_page, max_page, page_start, page_end,
     row_offset} =
      if server_paged? do
        total_rows =
          max(Map.get(aggregate_meta, :aggregate_total_rows, page_row_count), page_row_count)

        max_page =
          if total_rows > 0 and per_page > 0 do
            div(total_rows - 1, per_page)
          else
            0
          end

        current_page =
          aggregate_meta
          |> Map.get(:aggregate_page, Map.get(assigns, :aggregate_page, 0))
          |> normalize_page()
          |> min(max_page)

        row_offset = current_page * per_page

        page_start =
          if total_rows > 0 do
            row_offset + 1
          else
            0
          end

        page_end =
          if total_rows > 0 do
            min(row_offset + page_row_count, total_rows)
          else
            0
          end

        {rollup_rows, total_rows, current_page, max_page, page_start, page_end, row_offset}
      else
        max_page =
          if page_row_count > 0 and per_page > 0 do
            div(page_row_count - 1, per_page)
          else
            0
          end

        current_page =
          assigns
          |> Map.get(:aggregate_page, 0)
          |> normalize_page()
          |> min(max_page)

        row_offset = current_page * per_page

        page_start =
          if page_row_count > 0 do
            row_offset + 1
          else
            0
          end

        page_end =
          if page_row_count > 0 do
            min(row_offset + per_page, page_row_count)
          else
            0
          end

        paged_rows =
          if per_page_setting == "all" do
            rollup_rows
          else
            Enum.slice(rollup_rows, row_offset, per_page)
          end

        {paged_rows, page_row_count, current_page, max_page, page_start, page_end, row_offset}
      end

    paged_rollup_rows =
      prepend_continued_group_headers(
        paged_rollup_rows,
        num_group_by,
        length(aggregates_processed),
        row_offset
      )

    total_pages = if aggregate_total_rows > 0, do: max_page + 1, else: 0

    display_group_axes = visible_group_axes(group_by)
    grid_enabled = truthy?(Map.get(aggregate_meta, :grid_enabled, false))

    grid_available? =
      grid_enabled and length(display_group_axes) == 2 and length(aggregates_processed) == 1

    grid_colorize = truthy?(Map.get(aggregate_meta, :grid_colorize, false))

    grid_color_scale =
      aggregate_meta
      |> Map.get(:grid_color_scale, Options.default_grid_color_scale_mode())
      |> Options.normalize_grid_color_scale_mode()

    grid_data =
      if grid_available?,
        do:
          build_grid_data(
            rollup_rows,
            num_group_by,
            group_by,
            aggregates_processed,
            grid_colorize,
            grid_color_scale,
            assigns.theme
          ),
        else: nil

    assigns =
      assign(assigns,
        presentation_context: Map.get(assigns, :presentation_context, %{}),
        rollup_rows: rollup_rows,
        paged_rollup_rows: paged_rollup_rows,
        num_group_by: num_group_by,
        group_by: group_by,
        display_group_headers: display_group_headers(group_by),
        display_group_axes: display_group_axes,
        aggregate: aggregates_processed,
        aggregate_server_paged?: server_paged?,
        aggregate_total_rows: aggregate_total_rows,
        aggregate_total_rows_before_cap: total_rows_before_cap,
        aggregate_rows_capped?: rows_capped?,
        aggregate_max_client_rows: max_client_rows,
        aggregate_page: current_page,
        aggregate_max_page: max_page,
        aggregate_total_pages: total_pages,
        aggregate_page_start: page_start,
        aggregate_page_end: page_end,
        grid_enabled: grid_enabled,
        grid_colorize: grid_colorize,
        grid_color_scale: grid_color_scale,
        grid_available?: grid_available?,
        grid_data: grid_data,
        grid_legend_colors: grid_palette(assigns.theme)
      )

    ~H"""
    <div>
      <div
        :if={!@grid_available?}
        class={Theme.slot(@theme, :panel) <> " mb-3 flex flex-wrap items-center justify-between gap-3 px-3 py-2"}
        style="background: var(--sc-surface-bg-alt);"
      >
        <div class={Theme.slot(@theme, :panel) <> " inline-flex items-center gap-1 p-1"} style="background: var(--sc-surface-bg);">
          <button
            type="button"
            phx-click="set_aggregate_page"
            phx-value-page={0}
            phx-target={@myself}
            data-selecto-results-page="first"
            class={Theme.slot(@theme, :button_secondary) <> " h-8 w-8 disabled:cursor-not-allowed disabled:opacity-40"}
            title="First page"
            aria-label="First page"
            disabled={@aggregate_page <= 0 or @aggregate_page_loading?}
          >
            <svg
              class="h-4 w-4"
              fill="none"
              viewBox="0 0 24 24"
              stroke-width="2"
              stroke="currentColor"
              aria-hidden="true"
            >
              <path
                stroke-linecap="round"
                stroke-linejoin="round"
                d="M18 18L12 12l6-6M10 18 4 12l6-6M4 6v12"
              />
            </svg>
          </button>

          <button
            type="button"
            phx-click="set_aggregate_page"
            phx-value-page={@aggregate_page - 1}
            phx-target={@myself}
            data-selecto-results-page="previous"
            class={Theme.slot(@theme, :button_secondary) <> " h-8 gap-1 px-2 text-sm disabled:cursor-not-allowed disabled:opacity-40"}
            title="Previous page"
            aria-label="Previous page"
            disabled={@aggregate_page <= 0 or @aggregate_page_loading?}
          >
            <svg
              class="h-4 w-4"
              fill="none"
              viewBox="0 0 24 24"
              stroke-width="2"
              stroke="currentColor"
              aria-hidden="true"
            >
              <path stroke-linecap="round" stroke-linejoin="round" d="m15 18-6-6 6-6" />
            </svg>
            Prev
          </button>

          <button
            type="button"
            phx-click="set_aggregate_page"
            phx-value-page={@aggregate_page + 1}
            phx-target={@myself}
            data-selecto-results-page="next"
            class={Theme.slot(@theme, :button_secondary) <> " h-8 gap-1 px-2 text-sm disabled:cursor-not-allowed disabled:opacity-40"}
            title="Next page"
            aria-label="Next page"
            disabled={@aggregate_page >= @aggregate_max_page or @aggregate_page_loading?}
          >
            Next
            <svg
              class="h-4 w-4"
              fill="none"
              viewBox="0 0 24 24"
              stroke-width="2"
              stroke="currentColor"
              aria-hidden="true"
            >
              <path stroke-linecap="round" stroke-linejoin="round" d="m9 6 6 6-6 6" />
            </svg>
          </button>

          <button
            type="button"
            phx-click="set_aggregate_page"
            phx-value-page={@aggregate_max_page}
            phx-target={@myself}
            data-selecto-results-page="last"
            class={Theme.slot(@theme, :button_secondary) <> " h-8 w-8 disabled:cursor-not-allowed disabled:opacity-40"}
            title="Last page"
            aria-label="Last page"
            disabled={@aggregate_page >= @aggregate_max_page or @aggregate_page_loading?}
          >
            <svg
              class="h-4 w-4"
              fill="none"
              viewBox="0 0 24 24"
              stroke-width="2"
              stroke="currentColor"
              aria-hidden="true"
            >
              <path
                stroke-linecap="round"
                stroke-linejoin="round"
                d="M6 18l6-6-6-6m8 12 6-6-6-6m6 0v12"
              />
            </svg>
          </button>
        </div>

        <div class="text-sm font-medium" style="color: var(--sc-text-secondary);">
          <span class="font-semibold tabular-nums">
            {@aggregate_page_start}-{@aggregate_page_end}
          </span>
          of <span class="font-semibold tabular-nums">{@aggregate_total_rows}</span>
          rows
        </div>

        <div class="text-xs tabular-nums" style="color: var(--sc-text-muted);">
          Page
          <span class="font-semibold">
            {if @aggregate_total_pages > 0, do: @aggregate_page + 1, else: 0}
          </span>
          of <span class="font-semibold">{@aggregate_total_pages}</span>
          <span :if={@aggregate_page_loading?} class="ml-2" style="color: var(--sc-accent);">
            Loading...
          </span>
        </div>
      </div>

      <div
        :if={@aggregate_rows_capped?}
        class="mb-3 rounded-md border px-3 py-2 text-sm"
        style="background: color-mix(in srgb, var(--sc-accent-soft) 40%, var(--sc-surface-bg)); border-color: var(--sc-surface-border); color: var(--sc-text-primary);"
      >
        Showing the first <span class="font-semibold tabular-nums">{@aggregate_total_rows}</span>
        rows out of <span class="font-semibold tabular-nums">{@aggregate_total_rows_before_cap}</span>
        to keep rendering responsive. Narrow filters/grouping, or increase
          <code class="rounded px-1 py-0.5" style="background: var(--sc-surface-bg);">
            :aggregate_max_client_rows
          </code>
        (currently <span class="font-semibold">{inspect(@aggregate_max_client_rows)}</span>)
        if you need to render more rows at once.
      </div>

      <div
        :if={@grid_enabled and not @grid_available?}
        class="mb-3 rounded-md border px-3 py-2 text-sm"
        style="background: color-mix(in srgb, var(--sc-accent-soft) 50%, var(--sc-surface-bg)); border-color: var(--sc-surface-border); color: var(--sc-text-primary);"
      >
        Grid view requires exactly 2 Group By axes and 1 Aggregate.
      </div>

      <%= if @grid_available? and @grid_data do %>
        <div class="mb-2 flex flex-wrap items-center gap-2 text-sm" style="color: var(--sc-text-secondary);">
          <span class="font-medium">Aggregate Grid</span>
          <span
            :if={@grid_colorize}
            class="rounded-full border px-2 py-0.5 text-xs font-medium"
            style="border-color: var(--sc-surface-border); background: var(--sc-surface-bg); color: var(--sc-text-secondary);"
          >
            {String.capitalize(@grid_color_scale)} color scale
          </span>
        </div>
        <div
          :if={@grid_colorize}
          class="mb-3 flex flex-wrap items-center gap-2 rounded-md border px-3 py-2 text-xs"
          style="background: color-mix(in srgb, var(--sc-accent-soft) 45%, var(--sc-surface-bg)); border-color: var(--sc-surface-border); color: var(--sc-text-secondary);"
        >
          <span class="font-medium">Color legend</span>
          <span>Low</span>
          <div class="flex items-center gap-1" aria-label="Grid color legend">
            <span
              :for={color <- @grid_legend_colors}
              class="h-3 w-5 rounded-sm border"
              style={"border-color: var(--sc-surface-border); background-color: #{color};"}
            />
          </div>
          <span>High</span>
        </div>
        <div
          id={"aggregate-grid-wrapper-#{@myself}"}
          class={Theme.slot(@theme, :panel) <> " mb-1 max-h-[70vh] overflow-x-auto overflow-y-auto"}
        >
          <table class="min-w-full table-auto">
            <thead style="background: var(--sc-surface-bg-alt);">
              <tr>
                <th class="sticky left-0 top-0 z-30 px-3 py-3.5 text-left text-sm font-semibold" style="background: var(--sc-surface-bg-alt); color: var(--sc-text-primary); box-shadow: 1px 0 0 0 var(--sc-surface-border);">
                  {@grid_data.row_alias}
                </th>
                <%= for col_value <- @grid_data.col_headers do %>
                  <th class="sticky top-0 z-20 px-3 py-3.5 text-left text-sm font-semibold" style="background: var(--sc-surface-bg-alt); color: var(--sc-text-primary);">
                    <div
                      phx-click="agg_add_filters"
                      {build_axis_filter_attrs(@grid_data.col_axis, col_value)}
                      data-selecto-result-action
                      class="cursor-pointer whitespace-nowrap hover:underline"
                    >
                      <span class={null_grid_text_class(format_group_value(col_value, @grid_data.col_coldef, @presentation_context))}>
                        {format_group_value(col_value, @grid_data.col_coldef, @presentation_context)}
                      </span>
                    </div>
                  </th>
                <% end %>
              </tr>
            </thead>
            <tbody style="background: var(--sc-surface-bg); border-color: var(--sc-surface-border);">
              <tr
                :for={{row_value, row_idx} <- Enum.with_index(@grid_data.row_headers)}
                data-selecto-result-row
                data-result-row-index={row_idx}
              >
                <td
                  class="sticky left-0 z-10 px-3 py-2 text-sm font-semibold"
                  style="background: var(--sc-surface-bg); color: var(--sc-text-primary); box-shadow: 1px 0 0 0 var(--sc-surface-border);"
                  data-selecto-result-cell
                  data-result-row-index={row_idx}
                  data-result-column-index="0"
                  tabindex="-1"
                >
                  <div
                    phx-click="agg_add_filters"
                    {build_axis_filter_attrs(@grid_data.row_axis, row_value)}
                    data-selecto-result-action
                    class="cursor-pointer whitespace-nowrap hover:underline"
                  >
                    <span class={null_grid_text_class(format_group_value(row_value, @grid_data.row_coldef, @presentation_context))}>
                      {format_group_value(row_value, @grid_data.row_coldef, @presentation_context)}
                    </span>
                  </div>
                </td>
          <td
            :for={{col_value, col_idx} <- Enum.with_index(@grid_data.col_headers)}
            class="px-3 py-2 text-sm"
            style={Map.get(@grid_data.cell_styles, {row_value, col_value}, "background: var(--sc-surface-bg); color: var(--sc-text-primary);")}
            data-selecto-result-cell
            data-result-row-index={row_idx}
            data-result-column-index={col_idx + 1}
            tabindex="-1"
          >
                  <%= if Map.has_key?(@grid_data.cell_group_cols, {row_value, col_value}) do %>
                  <div
                    phx-click="agg_add_filters"
                    {build_filter_attrs(
                      Map.get(@grid_data.cell_group_cols, {row_value, col_value}, []),
                      @group_by,
                      @num_group_by
                    )}
                    data-selecto-result-action
                    class="cursor-pointer whitespace-nowrap hover:underline"
                  >
                    <span class={null_grid_text_class(format_aggregate_value(Map.get(@grid_data.cells, {row_value, col_value}), @grid_data.agg_coldef, @presentation_context))}>
                      {format_aggregate_value(Map.get(@grid_data.cells, {row_value, col_value}), @grid_data.agg_coldef, @presentation_context)}
                    </span>
                  </div>
                  <% else %>
                    <span class={null_grid_text_class(format_aggregate_value(Map.get(@grid_data.cells, {row_value, col_value}), @grid_data.agg_coldef, @presentation_context))}>
                      {format_aggregate_value(Map.get(@grid_data.cells, {row_value, col_value}), @grid_data.agg_coldef, @presentation_context)}
                    </span>
                  <% end %>
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      <% else %>
        <div
          id={"aggregate-table-wrapper-#{@myself}"}
          class={Theme.slot(@theme, :panel) <> " responsive-table-wrapper overflow-x-auto"}
        >
          <table class="min-w-full table-auto">
            <thead style="background: var(--sc-surface-bg-alt);">
              <tr>
                <%!-- Headers for group by columns --%>
                <%= for alias <- @display_group_headers do %>
                  <th class="px-3 py-3.5 text-left text-sm font-semibold" style="color: var(--sc-text-primary); border-bottom: 1px solid var(--sc-surface-border);">
                    {alias}
                  </th>
                <% end %>

                <%!-- Headers for aggregate columns --%>
                <%= for {alias, {:agg, _agg, _coldef}} <- @aggregate do %>
                  <th class="px-3 py-3.5 text-left text-sm font-semibold" style="color: var(--sc-text-primary); border-bottom: 1px solid var(--sc-surface-border);">
                    {alias}
                  </th>
                <% end %>
              </tr>
            </thead>
            <tbody style="background: var(--sc-surface-bg); border-color: var(--sc-surface-border);">
              <.rollup_row
                :for={{{level, row, continued?, grand_total?}, row_index} <- Enum.with_index(@paged_rollup_rows)}
                level={level}
                row={row}
                row_index={row_index}
                continued?={continued?}
                grand_total?={grand_total?}
                num_group_by={@num_group_by}
                group_by={@group_by}
                aggregate={@aggregate}
                presentation_context={@presentation_context}
                theme={@theme}
              />
            </tbody>
          </table>
        </div>
      <% end %>
    </div>
    """
  end

  defp truthy?(value) when value in [true, "true", "on", "1", 1], do: true
  defp truthy?(_), do: false

  defp build_grid_data(
         paged_rollup_rows,
         num_group_by,
         group_by,
         aggregates,
         colorize?,
         scale_mode,
         theme
       ) do
    detail_rows =
      Enum.reduce(paged_rollup_rows, [], fn
        {level, row, grand_total?}, acc when level == num_group_by ->
          if grand_total? do
            acc
          else
            [row | acc]
          end

        {level, row, continued?, grand_total?}, acc when level == num_group_by ->
          if continued? or grand_total? do
            acc
          else
            [row | acc]
          end

        _other, acc ->
          acc
      end)
      |> Enum.reverse()

    display_group_axes = visible_group_axes(group_by)
    row_axis = Enum.at(display_group_axes, 0, default_grid_axis("Group 1"))
    col_axis = Enum.at(display_group_axes, 1, default_grid_axis("Group 2"))

    {row_headers, col_headers, cells, cell_group_cols} =
      Enum.reduce(detail_rows, {[], [], %{}, %{}}, fn row,
                                                      {row_acc, col_acc, cells_acc,
                                                       group_cols_acc} ->
        row_value = grid_axis_value(row, row_axis)
        col_value = grid_axis_value(row, col_axis)
        agg_value = Enum.at(row, num_group_by)
        group_cols = Enum.take(row, num_group_by)

        row_acc = if row_value in row_acc, do: row_acc, else: row_acc ++ [row_value]
        col_acc = if col_value in col_acc, do: col_acc, else: col_acc ++ [col_value]
        cells_acc = Map.put(cells_acc, {row_value, col_value}, agg_value)
        group_cols_acc = Map.put(group_cols_acc, {row_value, col_value}, group_cols)

        {row_acc, col_acc, cells_acc, group_cols_acc}
      end)

    row_coldef = grid_axis_coldef(row_axis)
    col_coldef = grid_axis_coldef(col_axis)
    agg_coldef = grid_coldef(aggregates, 0)

    row_headers = sort_group_values(row_headers, row_coldef)
    col_headers = sort_group_values(col_headers, col_coldef)
    cell_styles = build_grid_cell_styles(cells, colorize?, scale_mode, theme)

    %{
      row_alias: row_axis.alias,
      row_axis: row_axis,
      col_axis: col_axis,
      row_headers: row_headers,
      col_headers: col_headers,
      cells: cells,
      cell_group_cols: cell_group_cols,
      cell_styles: cell_styles,
      row_coldef: row_coldef,
      col_coldef: col_coldef,
      agg_coldef: agg_coldef
    }
  end

  defp build_grid_cell_styles(cells, false, _scale_mode, _theme) when is_map(cells) do
    Enum.into(cells, %{}, fn {key, _value} ->
      {key, "background: var(--sc-surface-bg); color: var(--sc-text-primary);"}
    end)
  end

  defp build_grid_cell_styles(cells, true, scale_mode, theme) when is_map(cells) do
    max_positive_value =
      cells
      |> Map.values()
      |> Enum.reduce(nil, fn value, acc ->
        case numeric_value(value) do
          num when is_number(num) and num > 0 -> if(is_nil(acc), do: num, else: max(acc, num))
          _ -> acc
        end
      end)

    Enum.into(cells, %{}, fn {key, value} ->
      {key, grid_cell_style(value, max_positive_value, scale_mode, theme)}
    end)
  end

  defp grid_cell_style(value, max_positive_value, scale_mode, theme) do
    case grid_palette_color(value, max_positive_value, scale_mode, theme) do
      nil -> "background: var(--sc-surface-bg); color: var(--sc-text-primary);"
      color -> "background-color: #{color}; color: var(--sc-text-primary);"
    end
  end

  defp grid_palette_color(_value, nil, _scale_mode, _theme), do: nil

  defp grid_palette_color(value, max_positive_value, scale_mode, theme) do
    with num when is_number(num) <- numeric_value(value),
         true <- num > 0,
         true <- max_positive_value > 0 do
      palette = grid_palette(theme)

      ratio =
        case Options.normalize_grid_color_scale_mode(scale_mode) do
          "log" -> :math.log(num + 1) / :math.log(max_positive_value + 1)
          _ -> num / max_positive_value
        end

      palette_index =
        ratio
        |> Kernel.*(length(palette))
        |> Float.ceil()
        |> trunc()
        |> min(length(palette))
        |> max(1)

      Enum.at(palette, palette_index - 1)
    else
      _ -> nil
    end
  end

  defp grid_palette(_theme) do
    [
      "color-mix(in srgb, var(--sc-accent) 10%, var(--sc-surface-bg))",
      "color-mix(in srgb, var(--sc-accent) 16%, var(--sc-surface-bg))",
      "color-mix(in srgb, var(--sc-accent) 22%, var(--sc-surface-bg))",
      "color-mix(in srgb, var(--sc-accent) 28%, var(--sc-surface-bg))",
      "color-mix(in srgb, var(--sc-accent) 34%, var(--sc-surface-bg))",
      "color-mix(in srgb, var(--sc-accent) 40%, var(--sc-surface-bg))",
      "color-mix(in srgb, var(--sc-accent) 46%, var(--sc-surface-bg))",
      "color-mix(in srgb, var(--sc-accent) 52%, var(--sc-surface-bg))",
      "color-mix(in srgb, var(--sc-accent) 58%, var(--sc-surface-bg))",
      "color-mix(in srgb, var(--sc-accent) 64%, var(--sc-surface-bg))"
    ]
  end

  defp numeric_value(value) when is_integer(value), do: value
  defp numeric_value(value) when is_float(value), do: value

  defp numeric_value(value) when is_binary(value) do
    case Float.parse(value) do
      {parsed, ""} -> parsed
      _ -> nil
    end
  end

  defp numeric_value(_value), do: nil

  defp selected_field_alias({_kind, _field, alias_name}, _fallback)
       when is_binary(alias_name) and alias_name != "",
       do: alias_name

  defp selected_field_alias(_selected_field, fallback), do: fallback

  defp grid_coldef(group_by, idx) do
    case Enum.at(group_by, idx) do
      {_alias, {:group_by, _field, coldef}} -> coldef
      {_alias, {:agg, _field, coldef}} -> coldef
      _ -> %{}
    end
  end

  defp grid_axis_coldef(%{defs: [{_alias, {:group_by, _field, coldef}}]}), do: coldef

  defp grid_axis_coldef(%{defs: defs}) when is_list(defs) do
    %{
      linked_defs: Enum.map(defs, fn {_alias, {:group_by, _field, coldef}} -> coldef end)
    }
  end

  defp grid_axis_coldef(_axis), do: %{}

  defp default_grid_axis(alias_name) do
    %{
      alias: alias_name,
      defs: [],
      start_idx: nil,
      end_idx: nil
    }
  end

  defp grid_axis_value(row, %{start_idx: start_idx, end_idx: end_idx})
       when is_list(row) and is_integer(start_idx) and is_integer(end_idx) do
    values = Enum.slice(row, start_idx, end_idx - start_idx + 1)

    case values do
      [single] -> single
      many -> many
    end
  end

  defp grid_axis_value(_row, _axis), do: nil

  defp format_group_value(value, coldef, presentation_context) do
    if linked_grid_axis?(coldef) and is_list(value) do
      value
      |> Enum.zip(Map.get(coldef, :linked_defs, []))
      |> Enum.map(fn {part_value, part_coldef} ->
        format_group_value(part_value, part_coldef, presentation_context)
      end)
      |> Enum.join(" / ")
    else
      display_value = display_value_for_group(value, coldef)

      case Map.get(coldef || %{}, :group_format) || Map.get(coldef || %{}, "group_format") do
        "D" ->
          weekday_name(display_value)

        format when is_binary(format) and format != "" ->
          format_value(display_value, nil, presentation_context)

        _ ->
          format_value(display_value, coldef, presentation_context)
      end
    end
  end

  defp linked_grid_axis?(coldef) when is_map(coldef),
    do: is_list(Map.get(coldef, :linked_defs)) and Map.get(coldef, :linked_defs) != []

  defp linked_grid_axis?(_coldef), do: false

  defp display_value_for_group(value, coldef) do
    if composite_group_value?(coldef) do
      case value do
        {display_value, _filter_value} -> display_value
        [display_value, _filter_value] -> display_value
        _ -> value
      end
    else
      value
    end
  end

  defp filter_value_for_group(value, field, coldef) do
    extracted_value =
      if composite_group_value?(coldef) or match?({:row, _fields, _alias}, field) do
        case value do
          {_display_value, filter_value} -> filter_value
          [_display_value, filter_value] -> filter_value
          _ -> value
        end
      else
        value
      end

    case extracted_value do
      nil -> "__NULL__"
      "" -> "__NULL__"
      "[NULL]" -> "__NULL__"
      _ -> extracted_value
    end
  end

  defp composite_group_value?(coldef) do
    join_mode = Map.get(coldef || %{}, :join_mode) || Map.get(coldef || %{}, "join_mode")

    group_by_filter_select =
      Map.get(coldef || %{}, :group_by_filter_select) ||
        Map.get(coldef || %{}, "group_by_filter_select")

    join_mode in [:lookup, :star, :tag] or not is_nil(group_by_filter_select)
  end

  defp null_grid_text_class("[NULL]"), do: nil
  defp null_grid_text_class(_value), do: nil

  defp weekday_name(value) do
    case parse_int(value) do
      1 -> "Sunday"
      2 -> "Monday"
      3 -> "Tuesday"
      4 -> "Wednesday"
      5 -> "Thursday"
      6 -> "Friday"
      7 -> "Saturday"
      _ -> format_value(value)
    end
  end

  defp parse_int(v) when is_integer(v), do: v

  defp parse_int(v) when is_binary(v) do
    case Integer.parse(v) do
      {num, ""} -> num
      _ -> nil
    end
  end

  defp parse_int(_), do: nil

  defp configured_column(_selecto, nil), do: nil

  defp configured_column(selecto, key) do
    columns = Selecto.columns(selecto)

    Map.get(columns, key) ||
      case key do
        value when is_atom(value) ->
          Map.get(columns, Atom.to_string(value))

        value when is_binary(value) ->
          case safe_existing_atom(value) do
            nil -> nil
            atom_key -> Map.get(columns, atom_key)
          end

        _ ->
          nil
      end
  end

  defp safe_existing_atom(value) when is_binary(value) do
    String.to_existing_atom(value)
  rescue
    ArgumentError -> nil
  end

  defp maybe_normalized_column(nil), do: nil
  defp maybe_normalized_column(column_def), do: Selecto.Presentation.normalize_column(column_def)

  defp sort_group_values(values, coldef) when is_list(values) do
    format = Map.get(coldef || %{}, :group_format) || Map.get(coldef || %{}, "group_format")

    case format do
      "D" -> Enum.sort_by(values, &weekday_sort_key/1)
      "HH24" -> Enum.sort_by(values, &int_sort_key/1)
      "MM" -> Enum.sort_by(values, &int_sort_key/1)
      "DD" -> Enum.sort_by(values, &int_sort_key/1)
      _ -> values
    end
  end

  defp weekday_sort_key(value) do
    case parse_int(value) do
      int when is_integer(int) and int >= 1 and int <= 7 -> {0, int}
      _ -> {1, to_string(value)}
    end
  end

  defp int_sort_key(value) do
    case parse_int(value) do
      int when is_integer(int) -> {0, int}
      _ -> {1, to_string(value)}
    end
  end

  defp maybe_set_group_by_filter(coldef, field_name)
       when is_map(coldef) and is_binary(field_name) and field_name != "" do
    if join_mode_group?(coldef) do
      coldef
    else
      coldef
      |> Map.put_new(:group_by_filter, field_name)
      |> Map.put_new("group_by_filter", field_name)
    end
  end

  defp maybe_set_group_by_filter(coldef, _), do: coldef

  defp join_mode_group?(coldef) when is_map(coldef) do
    mode = Map.get(coldef, :join_mode) || Map.get(coldef, "join_mode")
    id_field = Map.get(coldef, :id_field) || Map.get(coldef, "id_field")

    mode in [:lookup, :star, :tag, "lookup", "star", "tag"] and not is_nil(id_field)
  end

  defp maybe_set_group_by_format(coldef, cfg) when is_map(coldef) and is_map(cfg) do
    format = Map.get(cfg, "format") || Map.get(cfg, :format)

    if is_binary(format) and format != "" do
      coldef
      |> Map.put(:group_format, format)
      |> Map.put("group_format", format)
      |> maybe_set_linked_to_next(cfg)
    else
      maybe_set_linked_to_next(coldef, cfg)
    end
  end

  defp maybe_set_group_by_format(coldef, _), do: coldef

  defp maybe_set_linked_to_next(coldef, cfg) when is_map(coldef) and is_map(cfg) do
    linked? =
      Map.get(cfg, "linked_to_next", Map.get(cfg, :linked_to_next))
      |> truthy?()

    coldef
    |> Map.put(:linked_to_next, linked?)
    |> Map.put("linked_to_next", linked?)
  end

  defp maybe_set_linked_to_next(coldef, _cfg), do: coldef

  defp display_group_headers(group_by) when is_list(group_by) do
    group_by
    |> visible_group_axes()
    |> Enum.map(& &1.alias)
  end

  defp visible_group_axes(group_by) when is_list(group_by) do
    linked_group_ranges(group_by)
    |> Enum.map(fn {start_idx, end_idx} ->
      defs = Enum.slice(group_by, start_idx, end_idx - start_idx + 1)

      %{
        alias:
          defs
          |> Enum.map(fn {alias_name, _definition} -> alias_name end)
          |> Enum.join(" / "),
        defs: defs,
        start_idx: start_idx,
        end_idx: end_idx
      }
    end)
  end

  defp visible_group_axes(_group_by), do: []

  defp active_group_range_for_level(_group_by, 0), do: nil

  defp active_group_range_for_level(group_by, display_level)
       when is_list(group_by) and display_level > 0 do
    display_idx = display_level - 1

    linked_group_ranges(group_by)
    |> Enum.find(fn {start_idx, end_idx} ->
      display_idx >= start_idx and display_idx <= end_idx
    end)
  end

  defp active_group_range_for_level(_group_by, _display_level), do: nil

  defp linked_group_ranges(group_by) when is_list(group_by) do
    {ranges, current_start} =
      Enum.with_index(group_by)
      |> Enum.reduce({[], nil}, fn {{_alias, {:group_by, _field, coldef}}, idx},
                                   {ranges, current_start} ->
        current_start = if is_nil(current_start), do: idx, else: current_start

        if linked_to_next?(coldef) do
          {ranges, current_start}
        else
          {ranges ++ [{current_start, idx}], nil}
        end
      end)

    case current_start do
      nil -> ranges
      start_idx -> ranges ++ [{start_idx, max(length(group_by) - 1, start_idx)}]
    end
  end

  defp linked_to_next?(coldef) when is_map(coldef) do
    truthy?(Map.get(coldef, :linked_to_next, Map.get(coldef, "linked_to_next")))
  end

  defp linked_to_next?(_coldef), do: false

  defp row_group_blocks(group_by, group_cols) when is_list(group_by) and is_list(group_cols) do
    linked_group_ranges(group_by)
    |> Enum.map(fn {start_idx, end_idx} ->
      %{
        start_idx: start_idx,
        end_idx: end_idx,
        defs: Enum.slice(group_by, start_idx, end_idx - start_idx + 1),
        values: Enum.slice(group_cols, start_idx, end_idx - start_idx + 1)
      }
    end)
  end

  defp row_group_blocks(_group_by, _group_cols), do: []

  defp group_block_value(%{defs: defs, values: values}, presentation_context) do
    formatted_values =
      values
      |> Enum.zip(defs)
      |> Enum.take_while(fn {value, _definition} -> value not in [nil, ""] end)
      |> Enum.map(fn {value, {_alias, {:group_by, _field, coldef}}} ->
        format_group_value(value, coldef, presentation_context)
      end)

    case formatted_values do
      [] ->
        case Enum.zip(values, defs) do
          [{value, {_alias, {:group_by, _field, coldef}}} | _rest] ->
            format_group_value(value, coldef, presentation_context)

          _ ->
            ""
        end

      values_to_join ->
        Enum.join(values_to_join, " / ")
    end
  end

  defp group_block_value(_block, _presentation_context), do: ""

  defp display_group_block?(_block, nil), do: false

  defp display_group_block?(%{start_idx: start_idx, end_idx: end_idx}, {active_start, active_end}) do
    start_idx == active_start and end_idx == active_end
  end

  defp group_block_padding(%{start_idx: start_idx}, {active_start, _active_end}, indent_px)
       when start_idx == active_start,
       do: indent_px

  defp group_block_padding(_block, _active_group_range, _indent_px), do: 0

  @impl true
  def handle_event("set_aggregate_page", %{"page" => page_param}, socket) do
    page =
      page_param
      |> parse_page_param()
      |> normalize_page()
      |> clamp_aggregate_page_if_known(socket.assigns)

    if Map.get(socket.assigns, :aggregate_server_paged?, false) do
      send(self(), {:update_aggregate_page, page})

      {:noreply,
       assign(socket,
         aggregate_page_loading?: true,
         aggregate_requested_page: page
       )}
    else
      {:noreply, assign(socket, :aggregate_page, page)}
    end
  end

  defp parse_page_param(page_param) when is_binary(page_param) do
    case Integer.parse(page_param) do
      {page, ""} -> page
      _ -> 0
    end
  end

  defp parse_page_param(page_param) when is_integer(page_param), do: page_param
  defp parse_page_param(_page_param), do: 0

  defp normalize_page(page) when is_integer(page), do: max(page, 0)
  defp normalize_page(_page), do: 0

  defp clamp_aggregate_page_if_known(page, assigns) do
    case Map.get(assigns, :aggregate_max_page) do
      max_page when is_integer(max_page) and max_page >= 0 -> min(page, max_page)
      _ -> page
    end
  end

  defp execution_error?(assigns) do
    case Map.get(assigns, :execution_error) do
      nil -> false
      "" -> false
      _ -> true
    end
  end

  defp present?(value) when is_binary(value), do: String.trim(value) != ""
  defp present?(_), do: false
end