lib/selecto_components/form.ex

defmodule SelectoComponents.Form do
  use Phoenix.LiveComponent

  import SelectoComponents.Components.Common
  alias Phoenix.LiveView.JS
  alias SelectoComponents.ErrorHandling.ErrorDisplay
  alias SelectoComponents.Form.FilterRendering
  alias SelectoComponents.Views.Runtime, as: ViewRuntime

  @doc """
  Form for configuing Selecto View

  attrs:
  selecto: the selecto structure
  view_config: attr which contains the data to draw the view

  """

  @impl true
  def render(assigns) do
    assigns =
      assign(assigns,
        columns: build_column_list(assigns.selecto),
        field_filters: FilterRendering.build_filter_list(assigns.selecto),
        use_saved_views: Map.get(assigns, :saved_view_module, false),
        form:
          Ecto.Changeset.cast({%{}, %{}}, assigns.view_config, []) |> to_form(as: "view_config")
      )

    ~H"""
    <div
      id={"selecto-form-#{@id}"}
      phx-hook=".ExportDownloads"
      class="border-solid border border-2 rounded-md border-gray-300 p-1 bg-base-100 text-base-content"
    >
      <.form for={@form} phx-change="view-validate" phx-submit="view-apply">
        <!-- Comprehensive Error Display Component -->
        <.live_component
          :if={Map.get(assigns, :execution_error) || Map.get(assigns, :component_errors, [])}
          module={ErrorDisplay}
          id="error_display"
          error={Map.get(assigns, :execution_error)}
          errors={Map.get(assigns, :component_errors, [])}
        />
        
    <!-- View Config Manager for saving/loading view configurations -->
        <.live_component
          :if={Map.get(assigns, :saved_view_config_module)}
          module={SelectoComponents.ViewConfigManager}
          id="view_config_manager"
          view_config={@view_config}
          saved_view_config_module={Map.get(assigns, :saved_view_config_module)}
          saved_view_context={
            SelectoComponents.Tenant.scoped_context(
              Map.get(assigns, :saved_view_context),
              Map.get(assigns, :tenant_context)
            )
          }
          current_user_id={Map.get(assigns, :current_user_id)}
          parent_id={@myself}
        />
        
    <!-- Main Navigation Tabs -->
        <div class="flex border-b border-gray-200 dark:border-gray-700 mb-4">
          <div class="flex space-x-1" role="tablist" aria-label="Configuration Sections">
            <button
              type="button"
              role="tab"
              aria-selected={@active_tab == "view" or @active_tab == nil}
              aria-controls="main-tabpanel-view"
              id="main-tab-view"
              phx-click={JS.push("set_active_tab", value: %{tab: "view"})}
              class={[
                "px-4 py-2 text-sm font-medium transition-all duration-200",
                "border-b-2 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary",
                if @active_tab == "view" or @active_tab == nil do
                  "border-primary text-primary bg-primary/5"
                else
                  "border-transparent text-gray-600 hover:text-gray-800 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200"
                end
              ]}
            >
              View
            </button>

            <button
              type="button"
              role="tab"
              aria-selected={@active_tab == "filter"}
              aria-controls="main-tabpanel-filter"
              id="main-tab-filter"
              phx-click={JS.push("set_active_tab", value: %{tab: "filter"})}
              class={[
                "px-4 py-2 text-sm font-medium transition-all duration-200",
                "border-b-2 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary",
                if @active_tab == "filter" do
                  "border-primary text-primary bg-primary/5"
                else
                  "border-transparent text-gray-600 hover:text-gray-800 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200"
                end
              ]}
            >
              Filters
            </button>

            <button
              :if={@use_saved_views}
              type="button"
              role="tab"
              aria-selected={@active_tab == "save"}
              aria-controls="main-tabpanel-save"
              id="main-tab-save"
              phx-click={JS.push("set_active_tab", value: %{tab: "save"})}
              class={[
                "px-4 py-2 text-sm font-medium transition-all duration-200",
                "border-b-2 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary",
                if @active_tab == "save" do
                  "border-primary text-primary bg-primary/5"
                else
                  "border-transparent text-gray-600 hover:text-gray-800 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200"
                end
              ]}
            >
              Save View
            </button>

            <button
              type="button"
              role="tab"
              aria-selected={@active_tab == "export"}
              aria-controls="main-tabpanel-export"
              id="main-tab-export"
              phx-click={JS.push("set_active_tab", value: %{tab: "export"})}
              class={[
                "px-4 py-2 text-sm font-medium transition-all duration-200",
                "border-b-2 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary",
                if @active_tab == "export" do
                  "border-primary text-primary bg-primary/5"
                else
                  "border-transparent text-gray-600 hover:text-gray-800 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200"
                end
              ]}
            >
              Export
            </button>
          </div>
        </div>
        
    <!-- Tab Content Panels -->
        <div
          role="tabpanel"
          id="main-tabpanel-view"
          aria-labelledby="main-tab-view"
          class={
            if @active_tab == "view" or @active_tab == nil do
              "border-solid border rounded-md border-gray-300 p-3 bg-base-100 text-base-content"
            else
              "hidden"
            end
          }
        >
          <.live_component
            module={SelectoComponents.Components.Tabs}
            id="view_mode"
            fieldname="view_mode"
            view_mode={@view_config.view_mode}
            options={@views}
          >
            <:section :let={{id, _mod, _, _} = view}>
              <.live_component
                module={ViewRuntime.form_component(view)}
                id={"view_#{id}_form"}
                columns={@columns}
                view_config={@view_config}
                view={view}
                selecto={@selecto}
              />
            </:section>
          </.live_component>
        </div>

        <div
          role="tabpanel"
          id="main-tabpanel-filter"
          aria-labelledby="main-tab-filter"
          class={
            if @active_tab == "filter" do
              "border-solid border rounded-md border-gray-300 p-3 bg-base-100 text-base-content"
            else
              "hidden"
            end
          }
        >
          <!-- Filter Sets Component -->
          <.live_component
            :if={Map.get(assigns, :filter_sets_adapter)}
            module={SelectoComponents.Filter.FilterSets}
            id="filter_sets"
            user_id={Map.get(assigns, :user_id)}
            domain={
              SelectoComponents.Tenant.scoped_context(
                Map.get(assigns, :domain) || Map.get(assigns, :path),
                Map.get(assigns, :tenant_context)
              )
            }
            current_filters={@view_config.filters}
            filter_sets_adapter={Map.get(assigns, :filter_sets_adapter)}
          />

          <.live_component
            module={SelectoComponents.Components.TreeBuilder}
            id={"#{@id}_tree_builder_#{FilterRendering.hash_filter_structure(@view_config.filters)}"}
            available={FilterRendering.build_filter_list(@selecto)}
            filters={@view_config.filters}
          >
            <:filter_form :let={{uuid, index, section, filter_value}}>
              {FilterRendering.render_filter_form(assigns, uuid, index, section, filter_value)}
            </:filter_form>
          </.live_component>
        </div>

        <div
          :if={@use_saved_views}
          role="tabpanel"
          id="main-tabpanel-save"
          aria-labelledby="main-tab-save"
          class={
            if @active_tab == "save" do
              "border-solid border rounded-md border-gray-300 p-3 bg-base-100 text-base-content"
            else
              "hidden"
            end
          }
        >
          <h3 class="text-base-content font-medium mb-2">Save View Configuration</h3>
          <div class="space-y-4">
            <p class="text-sm text-gray-600 dark:text-gray-400">
              Save your current view configuration for later use.
            </p>
            <div class="flex items-center gap-2">
              <label for="save_as" class="text-sm font-medium">Save As:</label>
              <.sc_input name="save_as" id="save_as" placeholder="Enter view name..." class="flex-1" />
            </div>
          </div>
        </div>

        <div
          role="tabpanel"
          id="main-tabpanel-export"
          aria-labelledby="main-tab-export"
          class={
            if @active_tab == "export" do
              "border-solid border rounded-md border-gray-300 p-3 bg-base-100 text-base-content"
            else
              "hidden"
            end
          }
        >
          <h3 class="text-base-content font-medium mb-2">Export Options</h3>
          <div class="space-y-4">
            <p class="text-sm text-gray-600 dark:text-gray-400">
              Export current query results now:
            </p>

            <div class="flex flex-wrap gap-2">
              <.sc_button type="button" phx-click="export_data" phx-value-format="csv">
                Download CSV
              </.sc_button>
              <.sc_button type="button" phx-click="export_data" phx-value-format="json">
                Download JSON
              </.sc_button>
            </div>

            <p class="text-xs text-gray-500 dark:text-gray-400">
              Email delivery and scheduled exports are planned next.
            </p>
          </div>
        </div>

        <.sc_button>Submit</.sc_button>
      </.form>

      <%!-- Render modal if enabled and triggered --%>
      <%= if Map.get(assigns, :enable_modal_detail) && Map.get(assigns, :show_detail_modal) do %>
        <%= if custom_modal_component = Map.get(assigns, :detail_modal_component) do %>
          <.live_component
            module={custom_modal_component}
            id="detail-modal"
            detail_data={@modal_detail_data}
          />
        <% else %>
          <.live_component
            module={SelectoComponents.Modal.DetailModal}
            id="detail-modal"
            record={@modal_detail_data.record}
            current_index={@modal_detail_data.current_index}
            total_records={@modal_detail_data.total_records}
            records={@modal_detail_data.records}
            fields={@modal_detail_data.fields}
            related_data={@modal_detail_data.related_data}
            title="Record Details"
            size={:lg}
            navigation_enabled={true}
            edit_enabled={false}
          />
        <% end %>
      <% end %>

      <script :type={Phoenix.LiveView.ColocatedHook} name=".ExportDownloads">
        export default {
          mounted() {
            this.handleEvent("selecto_export_download", (payload) => {
              const filename = payload.filename || "selecto_export.txt";
              const mimeType = payload.mime_type || "text/plain;charset=utf-8";
              const content = payload.content || "";

              const blob = new Blob([content], { type: mimeType });
              const url = URL.createObjectURL(blob);
              const link = document.createElement("a");

              link.href = url;
              link.download = filename;
              document.body.appendChild(link);
              link.click();
              link.remove();

              setTimeout(() => URL.revokeObjectURL(url), 0);
            });
          }
        }
      </script>
    </div>
    """
  end

  @impl true
  def handle_event("datetime-filter-change", params, socket) do
    # This event is triggered when datetime filter comparison mode changes
    # We need to update the view config to reflect the change
    require Logger
    Logger.debug("Datetime filter change event received")

    # Extract UUID from _target or params
    uuid =
      params["uuid"] ||
        case get_in(params, ["_target"]) do
          ["filters", uuid_val, "comp"] -> uuid_val
          _ -> nil
        end

    # Get the new comparison value directly from filters
    new_comp = get_in(params, ["filters", uuid, "comp"])

    Logger.debug("UUID: #{uuid}, New comparison: #{new_comp}")

    # Update the view_config in the socket assigns
    updated_filters =
      socket.assigns.view_config.filters
      |> Enum.map(fn
        {u, section, filter} when u == uuid ->
          # Update the comparison operator and reset value when changing modes
          updated_filter =
            case new_comp do
              "BETWEEN" ->
                # Reset to empty values for between mode
                Map.merge(filter, %{
                  "comp" => new_comp,
                  "value" => nil,
                  "value_start" => nil,
                  "value_end" => nil
                })

              "DATE_BETWEEN" ->
                # Reset to empty values for date between mode
                Map.merge(filter, %{
                  "comp" => new_comp,
                  "value" => nil,
                  "value_start" => nil,
                  "value_end" => nil
                })

              "DATE=" ->
                # Keep existing value or set to today's date
                Map.merge(filter, %{
                  "comp" => new_comp,
                  "value" => filter["value"] || Date.utc_today()
                })

              "DATE!=" ->
                # Keep existing value or set to today's date
                Map.merge(filter, %{
                  "comp" => new_comp,
                  "value" => filter["value"] || Date.utc_today()
                })

              "SHORTCUT" ->
                # Default to "today" for shortcuts
                Map.merge(filter, %{"comp" => new_comp, "value" => "today"})

              "RELATIVE" ->
                # Default to empty for relative
                Map.merge(filter, %{"comp" => new_comp, "value" => ""})

              _ ->
                # Keep existing value for standard operators
                Map.put(filter, "comp", new_comp)
            end

          {u, section, updated_filter}

        other ->
          other
      end)

    updated_config = put_in(socket.assigns.view_config, [:filters], updated_filters)

    # Send the updated config to the parent LiveView
    send(self(), {:update_view_config, updated_config})

    # Also update the LiveComponent's state for immediate rendering
    {:noreply, assign(socket, view_config: updated_config)}
  end

  defmacro __using__(_opts \\ []) do
    quote do
      ### These run in the 'use'ing liveview's context

      # Import all event handlers (this brings in error handling, helpers, and all events)
      use SelectoComponents.Form.EventHandlers

      # Import utility functions for LiveView usage
      import SelectoComponents.Form, only: [dev_mode?: 0, sanitize_error_for_environment: 1]

      # All event handlers are now provided by SelectoComponents.Form.EventHandlers
      # which includes: ViewLifecycle, FilterOperations, DrillDown, ListOperations,
      # QueryOperations, ModalOperations, and ExportOperations
    end

    ### quote do
  end

  ### __using___

  defp build_column_list(selecto) do
    Map.values(Selecto.columns(selecto))
    |> Enum.sort(fn a, b -> a.name <= b.name end)
    |> Enum.map(fn c -> {c.colid, c.name, Map.get(c, :format)} end)
  end

  # defp build_available_fields(selecto) do
  #   Selecto.columns(selecto)
  #   |> Enum.map(fn {field_id, column} ->
  #     field_id_str = if is_atom(field_id), do: Atom.to_string(field_id), else: to_string(field_id)
  #     field_name = Map.get(column, :name, field_id_str)
  #     {field_id_str, %{name: field_name}}
  #   end)
  #   |> Map.new()
  # end

  # Helper to extract selected columns from params for pivot detection
  # This function is used both internally and by Selecto.AutoPivot

  def get_selected_columns_from_params(params) do
    view_mode = Map.get(params, "view_mode", "")

    case view_mode do
      "aggregate" ->
        group_by_cols =
          Map.get(params, "group_by", %{})
          |> Map.values()
          |> Enum.map(fn item -> Map.get(item, "field") end)

        aggregate_cols =
          Map.get(params, "aggregate", %{})
          |> Map.values()
          |> Enum.map(fn item -> Map.get(item, "field") end)

        group_by_cols ++ aggregate_cols

      "detail" ->
        # Handle the selected map structure from the UI
        selected_map = Map.get(params, "selected", %{})

        # Extract field names from the selected map
        Map.values(selected_map)
        |> Enum.map(fn item ->
          Map.get(item, "field")
        end)
        |> Enum.filter(&(&1 != nil))

      _ ->
        []
    end
  end

  # Environment detection helpers
  def dev_mode? do
    # Check if we're in dev or test environment
    # You can also use Mix.env() if available, or Application.get_env
    case Application.get_env(:selecto_components, :environment) do
      nil ->
        # Fall back to checking common indicators
        System.get_env("MIX_ENV") in ["dev", "test", nil]

      :prod ->
        false

      :production ->
        false

      _ ->
        true
    end
  end

  def sanitize_error_for_environment(error) do
    if dev_mode?() do
      # In dev mode, return the full error with all details
      error
    else
      # In production, sanitize the error to remove sensitive information
      attrs = %{
        type: map_get(error, :type, :query_error),
        message: get_safe_error_message(error),
        details: %{},
        query: nil,
        params: []
      }

      maybe_build_selecto_error(attrs)
    end
  end

  @doc false
  def selecto_error?(%{__struct__: module}) when module == Selecto.Error, do: true
  def selecto_error?(_), do: false

  @doc false
  def build_selecto_error(type, message, details \\ %{}) do
    maybe_build_selecto_error(%{
      type: type,
      message: message,
      details: details,
      query: nil,
      params: []
    })
  end

  defp get_safe_error_message(error) do
    type = map_get(error, :type, :query_error)

    # Return user-friendly messages based on error type
    case type do
      :connection_error ->
        "Unable to connect to the database. Please try again later."

      :query_error ->
        "An error occurred while processing your request. Please try again."

      :timeout_error ->
        "The request took too long to complete. Please try again with a simpler query."

      :permission_error ->
        "You don't have permission to access this data."

      :validation_error ->
        "The request contains invalid parameters. Please check your inputs."

      _ ->
        "An unexpected error occurred. Please try again or contact support if the problem persists."
    end
  end

  defp maybe_build_selecto_error(attrs) do
    if Code.ensure_loaded?(Selecto.Error) do
      try do
        struct(Selecto.Error, attrs)
      rescue
        _ -> attrs
      end
    else
      attrs
    end
  end

  defp map_get(map, key, default) when is_map(map), do: Map.get(map, key, default)
  defp map_get(_, _key, default), do: default

  @doc false
  def build_debug_data(assigns) do
    query_data = Map.get(assigns, :last_query_info, %{})

    # Extract row count from query_results
    row_count =
      case Map.get(assigns, :query_results) do
        {rows, _columns, _aliases} when is_list(rows) ->
          length(rows)

        [] ->
          # Initial state has empty list
          0

        nil ->
          0

        other ->
          # Try to handle other formats
          case other do
            list when is_list(list) -> length(list)
            _ -> 0
          end
      end

    %{
      query: Map.get(query_data, :sql),
      params: Map.get(query_data, :params, []),
      timing: Map.get(query_data, :timing),
      row_count: row_count,
      execution_plan: Map.get(query_data, :execution_plan)
    }
  end
end