lib/selecto_components/view_config_manager.ex

defmodule SelectoComponents.ViewConfigManager do
  @moduledoc """
  Component for managing saved view configurations with view type separation.
  Allows saving and loading configurations specific to each view type.
  """

  use Phoenix.LiveComponent
  alias Phoenix.LiveView.JS
  alias SelectoComponents.SafeAtom

  @impl true
  def mount(socket) do
    {:ok,
     assign(socket,
       show_save_dialog: false,
       show_load_menu: false,
       saved_configs: [],
       config_name: "",
       config_description: "",
       is_public: false
     )}
  end

  @impl true
  def update(assigns, socket) do
    socket =
      socket
      |> assign(assigns)
      |> maybe_load_saved_configs()

    {:ok, socket}
  end

  defp maybe_load_saved_configs(socket) do
    # Only load configs if not already loaded
    if Map.get(socket.assigns, :configs_loaded, false) do
      socket
    else
      socket
      |> load_saved_configs()
      |> assign(configs_loaded: true)
    end
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div class="mb-4 p-4 bg-gray-50 border border-gray-200 rounded-lg">
      <div class="flex items-center justify-between">
        <h3 class="text-lg font-medium text-gray-900">
          View Configuration - {get_view_type_label(@view_config.view_mode)} Mode
        </h3>
        <div class="flex items-center gap-2">
          <!-- Load button with dropdown -->
          <div class="relative">
            <button
              type="button"
              phx-click="toggle_load_menu"
              phx-target={@myself}
              class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
            >
              <svg
                class="-ml-0.5 mr-2 h-4 w-4"
                xmlns="http://www.w3.org/2000/svg"
                fill="none"
                viewBox="0 0 24 24"
                stroke="currentColor"
              >
                <path
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  stroke-width="2"
                  d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
                />
              </svg>
              Load View
              <svg
                class="-mr-1 ml-2 h-4 w-4"
                xmlns="http://www.w3.org/2000/svg"
                fill="none"
                viewBox="0 0 24 24"
                stroke="currentColor"
              >
                <path
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  stroke-width="2"
                  d="M19 9l-7 7-7-7"
                />
              </svg>
            </button>
            
    <!-- Load dropdown menu -->
            <div
              :if={@show_load_menu}
              class="origin-top-left absolute left-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 divide-y divide-gray-100 z-50"
              phx-click-away={JS.push("hide_load_menu", target: @myself)}
            >
              <div class="py-1">
                <div class="px-3 py-2 text-xs text-gray-500 uppercase tracking-wider">
                  {get_view_type_label(@view_config.view_mode)} Views
                </div>
                <%= if Enum.empty?(@saved_configs) do %>
                  <div class="px-3 py-2 text-sm text-gray-500 italic">
                    No saved {String.downcase(get_view_type_label(@view_config.view_mode))} views
                  </div>
                <% else %>
                  <%= for config <- @saved_configs do %>
                    <button
                      type="button"
                      phx-click="load_view_config"
                      phx-value-name={config.name}
                      phx-target={@myself}
                      class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900"
                    >
                      <div class="font-medium">{config.name}</div>
                      <%= if config.description do %>
                        <div class="text-xs text-gray-500 mt-1">{config.description}</div>
                      <% end %>
                      <div class="text-xs text-gray-400 mt-1">
                        Updated {format_time_ago(config.updated_at)}
                        <%= if config.user_id do %>
                          • Private
                        <% else %>
                          • Public
                        <% end %>
                      </div>
                    </button>
                  <% end %>
                <% end %>
              </div>
            </div>
          </div>
          
    <!-- Save button -->
          <button
            type="button"
            phx-click={JS.push("show_save_dialog", target: @myself)}
            class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
          >
            <svg
              class="-ml-0.5 mr-2 h-4 w-4"
              xmlns="http://www.w3.org/2000/svg"
              fill="none"
              viewBox="0 0 24 24"
              stroke="currentColor"
            >
              <path
                stroke-linecap="round"
                stroke-linejoin="round"
                stroke-width="2"
                d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V2"
              />
            </svg>
            Save View
          </button>
        </div>
        
    <!-- Save dialog modal -->
        <%= if @show_save_dialog do %>
          <div
            class="fixed z-50 inset-0 overflow-y-auto"
            aria-labelledby="modal-title"
            role="dialog"
            aria-modal="true"
          >
            <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
              <!-- Background overlay -->
              <div
                class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
                aria-hidden="true"
                phx-click={JS.push("hide_save_dialog", target: @myself)}
              >
              </div>
              
    <!-- Modal panel -->
              <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
                <div>
                  <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
                    <div class="sm:flex sm:items-start">
                      <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
                        <svg
                          class="h-6 w-6 text-blue-600"
                          xmlns="http://www.w3.org/2000/svg"
                          fill="none"
                          viewBox="0 0 24 24"
                          stroke="currentColor"
                        >
                          <path
                            stroke-linecap="round"
                            stroke-linejoin="round"
                            stroke-width="2"
                            d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V2"
                          />
                        </svg>
                      </div>
                      <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
                        <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
                          Save {get_view_type_label(@view_config.view_mode)} View Configuration
                        </h3>
                        <div class="mt-4">
                          <label for="config_name" class="block text-sm font-medium text-gray-700">
                            Name <span class="text-red-500">*</span>
                          </label>
                          <input
                            type="text"
                            name="config_name"
                            id="config_name"
                            required
                            value={@config_name}
                            phx-change="update_config_name"
                            phx-target={@myself}
                            class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
                            placeholder="e.g., Weekly Report, Customer Analysis"
                          />
                        </div>
                        <div class="mt-4">
                          <label
                            for="config_description"
                            class="block text-sm font-medium text-gray-700"
                          >
                            Description
                          </label>
                          <textarea
                            name="config_description"
                            id="config_description"
                            rows="3"
                            value={@config_description}
                            phx-change="update_config_description"
                            phx-target={@myself}
                            class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
                            placeholder="Describe what this view shows..."
                          ><%= @config_description %></textarea>
                        </div>
                        <div class="mt-4">
                          <label class="inline-flex items-center">
                            <input
                              type="checkbox"
                              name="is_public"
                              phx-click="toggle_is_public"
                              phx-target={@myself}
                              checked={@is_public}
                              class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
                            />
                            <span class="ml-2 text-sm text-gray-600">
                              Make this view public (visible to all users)
                            </span>
                          </label>
                        </div>
                      </div>
                    </div>
                  </div>
                  <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
                    <button
                      type="button"
                      phx-click="do_save_view_config"
                      phx-target={@myself}
                      class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm"
                    >
                      Save View
                    </button>
                    <button
                      type="button"
                      phx-click={JS.push("hide_save_dialog", target: @myself)}
                      class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
                    >
                      Cancel
                    </button>
                  </div>
                </div>
              </div>
            </div>
          </div>
        <% end %>
      </div>
    </div>
    """
  end

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

  def handle_event("hide_save_dialog", _params, socket) do
    {:noreply,
     assign(socket,
       show_save_dialog: false,
       config_name: "",
       config_description: "",
       is_public: false
     )}
  end

  def handle_event("hide_load_menu", _params, socket) do
    {:noreply, assign(socket, show_load_menu: false)}
  end

  def handle_event("update_config_name", %{"config_name" => name}, socket) do
    {:noreply, assign(socket, config_name: name)}
  end

  def handle_event("update_config_description", %{"config_description" => desc}, socket) do
    {:noreply, assign(socket, config_description: desc)}
  end

  def handle_event("toggle_is_public", _params, socket) do
    {:noreply, assign(socket, is_public: !socket.assigns.is_public)}
  end

  def handle_event("do_save_view_config", _params, socket) do
    view_type = normalize_view_type(socket.assigns.view_config.view_mode || "detail")

    # Only save view-specific configuration (not filters)
    view_specific_config = extract_view_specific_config(socket.assigns.view_config, view_type)

    case socket.assigns.saved_view_config_module.save_view_config(
           socket.assigns.config_name,
           socket.assigns.saved_view_context,
           view_type,
           view_specific_config,
           user_id: Map.get(socket.assigns, :current_user_id),
           description: socket.assigns.config_description,
           is_public: socket.assigns[:is_public] || false
         ) do
      {:ok, _config} ->
        socket =
          socket
          |> assign(
            show_save_dialog: false,
            config_name: "",
            config_description: "",
            is_public: false
          )
          |> assign(configs_loaded: false)
          |> maybe_load_saved_configs()

        # Component can't use put_flash directly, but can update assigns
        # The parent can check for this and display a message
        {:noreply, socket}

      {:error, _changeset} ->
        # Component can't use put_flash directly
        {:noreply, socket}
    end
  end

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

  def handle_event("load_view_config", %{"name" => name}, socket) do
    view_type = normalize_view_type(socket.assigns.view_config.view_mode || "detail")

    config =
      socket.assigns.saved_view_config_module.load_view_config(
        name,
        socket.assigns.saved_view_context,
        view_type,
        user_id: Map.get(socket.assigns, :current_user_id)
      )

    case config do
      nil ->
        {:noreply, socket}

      config ->
        # Send message to parent LiveView to apply the config
        send(self(), {:apply_view_config, config})

        {:noreply,
         socket
         |> assign(show_load_menu: false)}
    end
  end

  defp load_saved_configs(socket) do
    if has_view_config_module?(socket) do
      view_type = normalize_view_type(socket.assigns.view_config.view_mode || "detail")

      configs =
        socket.assigns.saved_view_config_module.list_view_configs(
          socket.assigns.saved_view_context,
          view_type,
          user_id: Map.get(socket.assigns, :current_user_id),
          include_public: true
        )

      assign(socket, saved_configs: configs)
    else
      socket
    end
  end

  defp has_view_config_module?(socket) do
    Map.has_key?(socket.assigns, :saved_view_config_module) &&
      socket.assigns.saved_view_config_module != nil
  end

  defp extract_view_specific_config(view_config, view_type) do
    normalized_view_type = normalize_view_type(view_type)

    # Extract only the configuration for the current view type
    # Exclude filters as they have their own save system
    views = Map.get(view_config, :views, %{})

    view_type_atom = SafeAtom.to_view_mode(normalized_view_type)
    current_view_config = Map.get(views, view_type_atom, %{})

    # For detail view, ensure we have the actual selected columns from the view_config
    current_view_config =
      case normalized_view_type do
        "detail" ->
          # Get the selected columns from the main Selecto configuration
          selected = get_selected_from_selecto(view_config)
          order_by = get_order_by_from_selecto(view_config)

          current_view_config
          |> Map.put(:selected, selected)
          |> Map.put(:order_by, order_by)
          |> Map.put(:per_page, Map.get(current_view_config, :per_page, "30"))

        _ ->
          current_view_config
      end

    # Return only the view-specific configuration
    %{
      normalized_view_type => current_view_config
    }
    |> sanitize_for_json()
  end

  defp normalize_view_type(view_type) do
    view_type
    |> SafeAtom.to_view_mode()
    |> Atom.to_string()
  end

  defp get_selected_from_selecto(view_config) do
    # Try to get from the detail view first, then fall back to the main columns
    case get_in(view_config, [:views, :detail, :selected]) do
      nil ->
        # Fall back to columns from view_config
        Map.get(view_config, :columns, [])
        |> Enum.map(fn col ->
          case col do
            {uuid, field, data} -> {uuid, field, data}
            %{"uuid" => uuid, "field" => field} = data -> {uuid, field, data}
            _ -> nil
          end
        end)
        |> Enum.filter(&(&1 != nil))

      selected ->
        selected
    end
  end

  defp get_order_by_from_selecto(view_config) do
    # Try to get from the detail view first, then fall back
    case get_in(view_config, [:views, :detail, :order_by]) do
      nil ->
        # Fall back to order_by from view_config
        Map.get(view_config, :order_by, [])
        |> Enum.map(fn item ->
          case item do
            {uuid, field, data} -> {uuid, field, data}
            %{"uuid" => uuid, "field" => field} = data -> {uuid, field, data}
            _ -> nil
          end
        end)
        |> Enum.filter(&(&1 != nil))

      order_by ->
        order_by
    end
  end

  # defp view_config_to_params(view_config) when is_struct(view_config) do
  #   Map.from_struct(view_config)
  #   |> Map.drop([:__struct__, :__meta__])
  #   |> sanitize_for_json()
  # end

  # defp view_config_to_params(view_config) when is_map(view_config) do
  #   view_config
  #   |> sanitize_for_json()
  # end

  # Convert tuples to lists for JSON encoding
  defp sanitize_for_json(data) when is_map(data) do
    Map.new(data, fn {k, v} -> {k, sanitize_for_json(v)} end)
  end

  defp sanitize_for_json(data) when is_list(data) do
    Enum.map(data, &sanitize_for_json/1)
  end

  defp sanitize_for_json(data) when is_tuple(data) do
    data
    |> Tuple.to_list()
    |> sanitize_for_json()
  end

  defp sanitize_for_json(data), do: data

  defp get_view_type_label("aggregate"), do: "Aggregate"
  defp get_view_type_label("graph"), do: "Graph"
  defp get_view_type_label("map"), do: "Map"
  defp get_view_type_label(_), do: "Detail"

  defp format_time_ago(%DateTime{} = datetime) do
    now = DateTime.utc_now()
    diff = DateTime.diff(now, datetime, :second)

    cond do
      diff < 60 -> "just now"
      diff < 3600 -> "#{div(diff, 60)} min ago"
      diff < 86400 -> "#{div(diff, 3600)} hours ago"
      diff < 604_800 -> "#{div(diff, 86400)} days ago"
      true -> "#{div(diff, 604_800)} weeks ago"
    end
  end

  defp format_time_ago(%NaiveDateTime{} = naive_datetime) do
    datetime = DateTime.from_naive!(naive_datetime, "Etc/UTC")
    format_time_ago(datetime)
  end

  defp format_time_ago(nil), do: "unknown"
end