lib/selecto_components/views/aggregate/form.ex

defmodule SelectoComponents.Views.Aggregate.Form do
  use Phoenix.LiveComponent
  import SelectoComponents.Components.Common
  alias SelectoComponents.Views.Aggregate.Options
  alias SelectoComponents.Theme

  # Helper function to extract field from formatted date tuples
  defp get_field_for_item(selecto, item) do
    field_name =
      case item do
        # Handle formatted date tuple {:to_char, {"field_name", "format"}}
        {:to_char, {field, _format}} -> field
        # Handle other extraction tuples if any
        {_func, field} when is_binary(field) -> field
        # Regular field name
        field when is_binary(field) or is_atom(field) -> field
        # Fallback
        _ -> nil
      end

    if field_name do
      Selecto.field(selecto, field_name)
    else
      nil
    end
  end

  def render(assigns) do
    aggregate_view =
      assigns.view_config
      |> Map.get(:views, %{})
      |> Map.get(:aggregate, Map.get(Map.get(assigns.view_config, :views, %{}), "aggregate", %{}))

    aggregate_per_page = get_aggregate_per_page(assigns.view_config)
    aggregate_grid = get_aggregate_grid(assigns.view_config)
    aggregate_grid_colorize = get_aggregate_grid_colorize(assigns.view_config)
    aggregate_grid_color_scale = get_aggregate_grid_color_scale(assigns.view_config)

    assigns =
      assigns
      |> assign_new(:theme, fn -> Theme.default_theme(:light) end)
      |> assign(
        aggregate_view: aggregate_view,
        aggregate_per_page: aggregate_per_page,
        aggregate_grid: aggregate_grid,
        aggregate_grid_colorize: aggregate_grid_colorize,
        aggregate_grid_color_scale: aggregate_grid_color_scale,
        aggregate_grid_color_scale_options: Options.grid_color_scale_modes(),
        aggregate_per_page_options: Options.per_page_options()
      )

    ~H"""
    <div>
      <div class={Theme.slot(@theme, :panel) <> " mb-3 px-3 py-2"} style="background: var(--sc-surface-bg-alt);">
        <label for="aggregate_per_page" class="text-xs font-medium" style="color: var(--sc-text-secondary);">
          Aggregate Rows/Page
        </label>
        <.sc_select_with_slot theme={@theme} id="aggregate_per_page" name="aggregate_per_page" class="mt-1 w-36">
          <%= for option <- @aggregate_per_page_options do %>
            <option value={to_string(option)} selected={@aggregate_per_page == to_string(option)}>
              {if option == "all", do: "All", else: option}
            </option>
          <% end %>
        </.sc_select_with_slot>

        <label class="mt-3 inline-flex items-center gap-2 text-sm" style="color: var(--sc-text-secondary);">
          <input type="hidden" name="aggregate_grid" value="false" />
          <input
            type="checkbox"
            name="aggregate_grid"
            value="true"
            checked={@aggregate_grid}
            class="checkbox checkbox-sm"
            style="border-color: var(--sc-surface-border); color: var(--sc-accent);"
          />
          Grid view (2 group-by + 1 aggregate)
        </label>

        <label class="mt-3 inline-flex items-center gap-2 text-sm" style="color: var(--sc-text-secondary);">
          <input type="hidden" name="aggregate_grid_colorize" value="false" />
          <input
            type="checkbox"
            name="aggregate_grid_colorize"
            value="true"
            checked={@aggregate_grid_colorize}
            class="checkbox checkbox-sm"
            style="border-color: var(--sc-surface-border); color: var(--sc-accent);"
          />
          Colorize grid cells
        </label>

        <div class="mt-3 flex flex-wrap items-center gap-3">
          <label for="aggregate_grid_color_scale" class="text-xs font-medium" style="color: var(--sc-text-secondary);">
            Grid Color Scale
          </label>
          <.sc_select_with_slot theme={@theme} id="aggregate_grid_color_scale" name="aggregate_grid_color_scale" class="w-32">
            <%= for option <- @aggregate_grid_color_scale_options do %>
              <option value={option} selected={@aggregate_grid_color_scale == option}>
                {String.capitalize(option)}
              </option>
            <% end %>
          </.sc_select_with_slot>
        </div>
      </div>
      Group By
      <.live_component
        module={SelectoComponents.Components.ListPicker}
        id="group_by"
        theme={@theme}
        fieldname="group_by"
        view={@view}
        available={
          Enum.filter(@columns, fn {_f, _n, format} -> format not in [:component, :link] end)
        }
        selected_items={Map.get(@aggregate_view, :group_by, Map.get(@aggregate_view, "group_by", []))}
      >
        <:item_summary :let={{_id, item, config, _index}}>
          <% col = get_field_for_item(@selecto, item) %>
          <% format_summary = group_by_format_summary(col, config) %>
          <span class="truncate">{summary_title(config, column_display_name(@columns, item, col))}</span>
          <span :if={present_summary?(format_summary)} class="truncate text-sm font-normal text-base-content/60">{format_summary}</span>
        </:item_summary>
        <:item_form :let={{id, item, config, index}}>
          <input name={"group_by[#{id}][field]"} type="hidden" value={item} />
          <input name={"group_by[#{id}][index]"} type="hidden" value={index} />
          <.live_component
            module={SelectoComponents.Views.Aggregate.GroupByConfig}
            id={id}
            col={get_field_for_item(@selecto, item)}
            uuid={id}
            item={item}
            columns={@columns}
            fieldname="group_by"
            prefix={ "group_by[#{id}]" }
            config={config}
            theme={@theme}
          />
        </:item_form>
      </.live_component>
      Aggregates:
      <.live_component
        module={SelectoComponents.Components.ListPicker}
        id="aggregate"
        theme={@theme}
        fieldname="aggregate"
        view={@view}
        available={@columns}
        selected_items={Map.get(@aggregate_view, :aggregate, Map.get(@aggregate_view, "aggregate", []))}
      >
        <:item_summary :let={{_id, item, config, _index}}>
          <% col = get_field_for_item(@selecto, item) %>
          <span class="truncate">{summary_title(config, column_display_name(@columns, item, col))}</span>
          <span class="truncate text-sm font-normal text-base-content/60">{aggregate_format_summary(col, config)}</span>
        </:item_summary>
        <:item_form :let={{id, item, config, index}}>
          <input name={"aggregate[#{id}][field]"} type="hidden" value={item} />
          <input name={"aggregate[#{id}][index]"} type="hidden" value={index} />
          <.live_component
            module={SelectoComponents.Views.Aggregate.Aggregate.Config}
            id={id}
            col={get_field_for_item(@selecto, item)}
            uuid={id}
            item={item}
            columns={@columns}
            fieldname="aggregate"
            prefix={ "aggregate[#{id}]" }
            config={config}
            theme={@theme}
          />
        </:item_form>
      </.live_component>
    </div>
    """
  end

  defp get_aggregate_per_page(view_config) do
    view_config
    |> Map.get(:views, %{})
    |> Map.get(:aggregate, %{})
    |> then(fn aggregate_cfg ->
      Map.get(
        aggregate_cfg,
        :per_page,
        Map.get(aggregate_cfg, "per_page", Options.default_per_page())
      )
    end)
    |> Options.normalize_per_page_param()
  end

  defp get_aggregate_grid(view_config) do
    view_config
    |> Map.get(:views, %{})
    |> Map.get(:aggregate, %{})
    |> then(fn aggregate_cfg ->
      Map.get(aggregate_cfg, :grid, Map.get(aggregate_cfg, "grid", false))
    end)
    |> normalize_checkbox()
  end

  defp get_aggregate_grid_colorize(view_config) do
    view_config
    |> Map.get(:views, %{})
    |> Map.get(:aggregate, %{})
    |> then(fn aggregate_cfg ->
      Map.get(aggregate_cfg, :grid_colorize, Map.get(aggregate_cfg, "grid_colorize", false))
    end)
    |> normalize_checkbox()
  end

  defp get_aggregate_grid_color_scale(view_config) do
    view_config
    |> Map.get(:views, %{})
    |> Map.get(:aggregate, %{})
    |> then(fn aggregate_cfg ->
      Map.get(
        aggregate_cfg,
        :grid_color_scale,
        Map.get(aggregate_cfg, "grid_color_scale", Options.default_grid_color_scale_mode())
      )
    end)
    |> Options.normalize_grid_color_scale_mode()
  end

  defp normalize_checkbox(value) when value in [true, "true", "on", "1", 1], do: true
  defp normalize_checkbox(_value), do: false

  defp column_display_name(columns, item, col) do
    item_str =
      case item do
        {:to_char, {field, _format}} -> to_string(field)
        {_func, field} when is_binary(field) -> field
        value -> to_string(value || "")
      end

    case Enum.find(columns || [], fn
           {id, _name, _type} -> to_string(id) == item_str
           {id, _name, _type, _metadata} -> to_string(id) == item_str
           _ -> false
         end) do
      {_id, name, _type} -> name
      {_id, name, _type, _metadata} -> name
      _ -> if(col && Map.get(col, :name), do: col.name, else: item_str)
    end
  end

  defp summary_title(config, field_name) do
    case Map.get(config || %{}, "alias", "") do
      value when value in [nil, ""] -> field_name
      value -> "#{value} / #{field_name}"
    end
  end

  defp group_by_format_summary(col, config) do
    case config_value(config, :format) do
      value when value in [nil, ""] ->
        case Map.get(col || %{}, :type, :string) do
          x
          when x in [
                 :string,
                 :text,
                 :citext,
                 :int,
                 :id,
                 :decimal,
                 :float,
                 :integer,
                 :naive_datetime,
                 :utc_datetime,
                 :date
               ] ->
            nil

          _ ->
            "standard"
        end

      "default" ->
        nil

      "text_prefix" ->
        "text prefix"

      "age_buckets" ->
        "age buckets"

      "custom_buckets" ->
        "custom buckets"

      "year_buckets" ->
        "year buckets"

      value ->
        SelectoComponents.Helpers.datetime_grouping_format_label(value)
    end
  end

  defp aggregate_format_summary(col, config) do
    format =
      case config_value(config, :format) do
        value when value in [nil, ""] ->
          col
          |> aggregate_default_format()
          |> format_summary_label()

        value ->
          format_summary_label(value)
      end

    if config_value(config, :format) == "sum" and
         normalize_checkbox(config_value(config, :ignore_nulls_in_sum)) do
      "#{format}, null as 0"
    else
      format
    end
  end

  defp config_value(config, key) when is_map(config) and is_atom(key) do
    Map.get(config, Atom.to_string(key), Map.get(config, key))
  end

  defp config_value(_config, _key), do: nil

  defp aggregate_default_format(col) do
    case Map.get(col || %{}, :type, :string) do
      :float -> "avg"
      _ -> "count"
    end
  end

  defp format_summary_label(value) do
    SelectoComponents.Helpers.aggregate_datetime_format_label(value)
  end

  defp present_summary?(value), do: value not in [nil, ""]
end