# frozen_string_literal: true

# PartyQueryBuilder is responsible for building an ActiveRecord query for parties
# by applying a series of filters, includes, and excludes based on request parameters.
# It is used to build both the general parties query and specialized queries (like
# for a user’s profile) while keeping the filtering logic DRY.
#
# Usage:
#   base_query = Party.includes(:user, :job, ... )  # a starting query
#   query_builder = PartyQueryBuilder.new(base_query, params: params, current_user: current_user, options: { default_status: 'active' })
#   final_query = query_builder.build
#
class PartyQueryBuilder
  # Initialize with a base query, a params hash, and the current user.
  # Options may include default filters like :default_status, default counts, and max values.
  def initialize(base_query, params:, current_user:, options: {})
    @base_query = base_query
    @params = params
    @current_user = current_user
    @options = options
  end

  # Builds the final ActiveRecord query by applying filters, includes, and excludes.
  #
  # Edge cases handled:
  # - If a parameter is missing or blank, default values are used.
  # - If no recency is provided, no date range is applied.
  # - If includes/excludes parameters are missing, those methods are skipped.
  #
  # Also applies a default status filter (if provided via options) using a dedicated callback.
  def build
    query = @base_query
    query = apply_filters(query)
    query = apply_default_status(query) if @options[:default_status]
    query = apply_privacy_settings(query)
    query = apply_includes(query, @params[:includes]) if @params[:includes].present?
    query = apply_excludes(query, @params[:excludes]) if @params[:excludes].present?
    query.order(created_at: :desc)
  end

  private

  # Applies filtering conditions to the given query.
  # Combines generic filters (like element, raid_id, created_at) with object count ranges.
  #
  # Example edge case: If the request does not specify 'characters_count',
  # then the default (e.g. 3) will be used, with the upper bound coming from a constant.
  def apply_filters(query)
    conditions = build_filters
    query = query.where(conditions)
    # If name_quality filtering is enabled via params, apply it.
    query = query.where(name_quality) if @params[:name_quality].present?
    query.where(
      weapons_count: build_count(@params[:weapons_count], default_weapons_count)..max_weapons,
      characters_count: build_count(@params[:characters_count], default_characters_count)..max_characters,
      summons_count: build_count(@params[:summons_count], default_summons_count)..max_summons
    )
  end

  # Example callback method: if no explicit status filter is provided, we may want
  # to force the query to include only records with a given default status.
  # This method encapsulates that behavior.
  def apply_default_status(query)
    query.where(status: @options[:default_status])
  end

  # Applies privacy settings based on whether the current user is an admin.
  def apply_privacy_settings(query)
    # If the options say to skip privacy filtering (e.g. when viewing your own profile),
    # then return the query unchanged.
    return query if @options[:skip_privacy]

    # Otherwise, if not admin, only show public parties.
    return query if @current_user&.admin?

    query.where('visibility = ?', 1)
  end

  # Builds a hash of filtering conditions from the params.
  #
  # Uses guard clauses to ignore keys when a parameter is missing.
  def build_filters
    {
      element: (@params[:element].present? ? @params[:element].to_i : nil),
      raid_id: @params[:raid],
      created_at: build_date_range,
      full_auto: build_option(@params[:full_auto]),
      auto_guard: build_option(@params[:auto_guard]),
      charge_attack: build_option(@params[:charge_attack])
    }.compact
  end

  # Returns a date range based on the 'recency' parameter.
  # If recency is not provided, returns nil so no date filter is applied.
  def build_date_range
    return nil unless @params[:recency].present?
    start_time = DateTime.current - @params[:recency].to_i.seconds
    start_time.beginning_of_day..DateTime.current
  end

  # Returns the count from the parameter or a default value if the parameter is blank.
  def build_count(value, default_value)
    value.blank? ? default_value : value.to_i
  end

  # Processes an option parameter.
  # Returns the integer value unless the value is blank or equal to -1.
  def build_option(value)
    value.to_i unless value.blank? || value.to_i == -1
  end

  # Applies "includes" filtering to the query based on a comma-separated string.
  # For each provided ID, it adds a condition using an EXISTS subquery.
  #
  # Edge case example: If an ID does not start with a known prefix,
  # grid_table_and_object_table returns [nil, nil] and the condition is skipped.
  def apply_includes(query, includes)
    includes.split(',').each do |id|
      grid_table, object_table = grid_table_and_object_table(id)
      next unless grid_table && object_table
      condition = <<-SQL.squish
        EXISTS (
          SELECT 1 FROM #{grid_table}
          JOIN #{object_table} ON #{grid_table}.#{object_table.singularize}_id = #{object_table}.id
          WHERE #{object_table}.granblue_id = ? AND #{grid_table}.party_id = parties.id
        )
      SQL
      query = query.where(condition, id)
    end
    query
  end

  # Applies "excludes" filtering to the query based on a comma-separated string.
  # Works similarly to apply_includes, but with a NOT EXISTS clause.
  def apply_excludes(query, excludes)
    excludes.split(',').each do |id|
      grid_table, object_table = grid_table_and_object_table(id)
      next unless grid_table && object_table
      condition = <<-SQL.squish
        NOT EXISTS (
          SELECT 1 FROM #{grid_table}
          JOIN #{object_table} ON #{grid_table}.#{object_table.singularize}_id = #{object_table}.id
          WHERE #{object_table}.granblue_id = ? AND #{grid_table}.party_id = parties.id
        )
      SQL
      query = query.where(condition, id)
    end
    query
  end

  # Maps an ID’s first character to the corresponding grid table and object table names.
  #
  # For example:
  #   '3...' => %w[grid_characters characters]
  #   '2...' => %w[grid_summons summons]
  #   '1...' => %w[grid_weapons weapons]
  # Returns [nil, nil] for unknown prefixes.
  def grid_table_and_object_table(id)
    case id[0]
    when '3'
      %w[grid_characters characters]
    when '2'
      %w[grid_summons summons]
    when '1'
      %w[grid_weapons weapons]
    else
      [nil, nil]
    end
  end

  # Default values and maximum limits for counts.
  def default_weapons_count
    @options[:default_weapons_count] || 5;
  end

  def default_characters_count
    @options[:default_characters_count] || 3;
  end

  def default_summons_count
    @options[:default_summons_count] || 2;
  end

  def max_weapons
    @options[:max_weapons] || 13;
  end

  def max_characters
    @options[:max_characters] || 5;
  end

  def max_summons
    @options[:max_summons] || 8;
  end

  # Stub method for name quality filtering.
  # In your application, this might be defined in a helper or concern.
  def name_quality
    # Example: exclude parties with names like 'Untitled' (edge case)
    "name NOT LIKE 'Untitled%'"
  end

  # Stub method for user quality filtering.
  # Adjust as needed for your actual implementation.
  def user_quality
    'user_id IS NOT NULL'
  end

  # Stub method for original filtering.
  def original
    'source_party_id IS NULL'
  end

  # Stub method for privacy filtering.
  # Here we assume that if the current user is not an admin, only public parties (visibility = 1) are returned.
  def privacy
    return nil if @current_user && @current_user.admin?

    'visibility = 1'
  end
end
