app/controllers/avo/actions_controller.rb



require_dependency "avo/application_controller"

module Avo
  class ActionsController < ApplicationController
    before_action :set_resource_name, :set_resource
    before_action :set_query, :set_record, :set_action, :verify_authorization, only: [:show, :handle]
    before_action :set_fields, only: :handle

    # Sets @record based on context:
    # - If we're on a show page (with an :id param), defer to superclass logic
    # - If on an index page with exactly one query result, assign it directly
    def set_record
      if params[:id].present?
        super
      elsif @query.size == 1
        @record = @query.first
      end
    end

    layout :choose_layout

    def show
      # Se the view to :new so the default value gets prefilled
      @view = Avo::ViewInquirer.new("new")

      @resource.hydrate(record: @record, view: @view, user: _current_user, params: params)
      @fields = @action.get_fields

      build_background_url
    end

    def build_background_url
      uri = URI.parse(request.url)

      # Remove the "/actions" segment from the path
      path_without_actions = uri.path.sub("/actions", "")

      params = URI.decode_www_form(uri.query || "").to_h

      params.delete("action_id")
      params[:turbo_frame] = ACTIONS_BACKGROUND_FRAME_ID

      # Reconstruct the query string
      new_query_string = URI.encode_www_form(params)

      # Update the URI components
      uri.path = path_without_actions
      uri.query = (new_query_string == "") ? nil : new_query_string

      # Reconstruct the modified URL
      @background_url = uri.to_s
    end

    def handle
      performed_action = @action.handle_action(
        fields: @fields,
        current_user: _current_user,
        resource: @resource,
        query: @query
      )

      @response = performed_action.response
      respond
    end

    private

    def set_query
      # If the user selected all records, use the decrypted index query
      # Otherwise, find the records from the resource ids
      @query = if action_params[:fields]&.dig(:avo_selected_all) == "true"
        decrypted_index_query
      else
        find_records_from_resource_ids
      end
    end

    def find_records_from_resource_ids
      if (ids = action_params[:fields]&.dig(:avo_resource_ids)&.split(",") || []).any?
        @resource.find_record(ids, params: params)
      else
        []
      end
    end

    def set_fields
      @fields = action_params[:fields].except(:avo_resource_ids, :avo_index_query)
    end

    def action_params
      @action_params ||= params.permit(
        :id, :authenticity_token, :resource_name, :action_id, :button, :arguments, :view_type, :resource_view, fields: {}
      )
    end

    def set_action
      selected_action_class = action_class
      return if performed? || selected_action_class.nil?

      @action = selected_action_class.new(
        record: @record,
        resource: @resource,
        user: _current_user,
        # force the action view to in order to render new-related fields (hidden field)
        view: Avo::ViewInquirer.new(:new),
        arguments: BaseAction.decode_arguments(params[:arguments] || params.dig(:fields, :arguments)) || {},
        query: @query,
        index_query: decrypted_index_query
      )

      # Fetch action's fields
      @action.fields
    end

    def action_class
      @resource.hydrate(view: action_params[:resource_view].presence, user: _current_user, params: params)

      registered_action = @resource.find_action(params[:action_id])

      if registered_action.nil?
        if Rails.env.development?
          raise Avo::ActionNotRegisteredError.new(params[:action_id], @resource.class)
        end

        flash[:alert] = I18n.t("avo.failed")
        redirect_to request.referer || resources_path(resource: @resource)
        return
      end

      registered_action[:class]
    end

    def respond
      # Flash the messages collected from the action
      flash_messages

      # Always execute turbo_stream.avo_close_modal on all responses, including redirects
      # Exclude response types intended to keep the modal open
      # This ensures the modal frame refreshes, preventing it from retaining the SRC of the previous action
      # and avoids re-triggering that SRC during back navigation
      respond_to do |format|
        format.turbo_stream do
          turbo_response = case @response[:type]
          when :keep_modal_open
            # Only render the flash messages if the action keeps the modal open
            turbo_stream.avo_flash_alerts
          when :download
            # Trigger download, removes modal and flash the messages
            [
              turbo_stream.avo_download(content: Base64.encode64(@response[:path]), filename: @response[:filename]),
              turbo_stream.avo_close_modal,
              turbo_stream.avo_flash_alerts
            ]
          when :navigate_to_action
            src, _ = @response[:action].link_arguments(resource: @action.resource, **@response[:navigate_to_action_args])

            turbo_stream.turbo_frame_set_src(Avo::MODAL_FRAME_ID, src)
          when :redirect
            [
              turbo_stream.avo_close_modal,
              turbo_stream.redirect_to(
                Avo::ExecutionContext.new(target: @response[:path]).handle,
                turbo_frame: @response[:redirect_args][:turbo_frame],
                **@response[:redirect_args].except(:turbo_frame)
              )
            ]
          when :close_modal
            # Close the modal and flash the messages
            [
              turbo_stream.avo_close_modal,
              turbo_stream.avo_flash_alerts
            ]
          else
            # Reload the page
            back_path = request.referer || params[:referrer].presence || resources_path(resource: @resource)

            [
              turbo_stream.avo_close_modal,
              turbo_stream.redirect_to(back_path)
            ]
          end

          responses = if @action.appended_turbo_streams.present?
            Array(turbo_response) + Array(instance_exec(&@action.appended_turbo_streams))
          else
            Array(turbo_response)
          end

          render turbo_stream: responses
        end
      end
    end

    def get_messages
      default_message = {
        type: :info,
        body: I18n.t("avo.action_ran_successfully")
      }

      return [default_message] if @response[:messages].blank?

      @response[:messages].select do |message|
        # Remove the silent placeholder messages
        message[:type] != :silent
      end
    end

    def decrypted_index_query
      @decrypted_index_query ||= if encrypted_query.present? && encrypted_query != "select_all_disabled"
        Avo::Services::EncryptionService.decrypt(message: encrypted_query, purpose: :select_all, serializer: Marshal)
      end
    end

    def encrypted_query
      @encrypted_query ||= action_params[:fields]&.dig(:avo_index_query)
    end

    def flash_messages
      get_messages.each do |message|
        flash[message[:type]] = {
          body: message[:body],
          timeout: message[:timeout]
        }
      end
    end

    def verify_authorization
      raise Avo::NotAuthorizedError.new unless @action.authorized?
    end
  end
end