# frozen_string_literal: true

class CategoriesController < ApplicationController
  include TopicQueryParams

  requires_login except: %i[
                   index
                   categories_and_latest
                   categories_and_top
                   categories_and_hot
                   show
                   redirect
                   find_by_slug
                   visible_groups
                   find
                   search
                 ]

  before_action :fetch_category, only: %i[show update destroy visible_groups convert_nested_replies]
  before_action :initialize_staff_action_logger, only: %i[create update destroy]

  skip_before_action :check_xhr,
                     only: %i[
                       index
                       categories_and_latest
                       categories_and_top
                       categories_and_hot
                       redirect
                     ]
  skip_before_action :verify_authenticity_token, only: %i[search]

  # The front-end is POSTing data to this endpoint, but we're not modifying anything
  allow_in_readonly_mode :search

  SYMMETRICAL_CATEGORIES_TO_TOPICS_FACTOR = 1.5
  MIN_CATEGORIES_TOPICS = 5
  MAX_CATEGORIES_TOPICS = 100
  MAX_CATEGORIES_LIMIT = 25
  MAX_CATEGORY_SEARCH_TERM_LENGTH = 250
  MAX_CATEGORY_SEARCH_WORDS = 25

  def redirect
    return if handle_permalink("/category/#{params[:path]}")
    redirect_to path("/c/#{params[:path]}")
  end

  def index
    discourse_expires_in 1.minute

    @category_list = fetch_category_list

    respond_to do |format|
      format.html do
        @title =
          if current_homepage == "categories" && SiteSetting.short_site_description.present?
            "#{SiteSetting.title} - #{SiteSetting.short_site_description}"
          elsif current_homepage != "categories"
            "#{I18n.t("js.filters.categories.title")} - #{SiteSetting.title}"
          end

        @description = SiteSetting.site_description

        store_preloaded(
          @category_list.preload_key,
          MultiJson.dump(CategoryListSerializer.new(@category_list, scope: guardian)),
        )

        preload_topic_list

        render
      end

      format.json { render_serialized(@category_list, CategoryListSerializer) }
    end
  end

  def categories_and_latest
    categories_and_topics(:latest)
  end

  def categories_and_top
    categories_and_topics(:top)
  end

  def categories_and_hot
    categories_and_topics(:hot)
  end

  def move
    guardian.ensure_can_create_category!

    params.require("category_id")
    params.require("position")

    if category = Category.find(params["category_id"])
      guardian.ensure_can_see!(category)
      category.move_to(params["position"].to_i)
      render json: success_json
    else
      render status: :internal_server_error, json: failed_json
    end
  end

  def reorder
    guardian.ensure_can_create_category!

    params.require(:mapping)
    change_requests = MultiJson.load(params[:mapping])
    by_category = Hash[change_requests.map { |cat, pos| [Category.find(cat.to_i), pos] }]

    unless guardian.is_admin?
      unless by_category.keys.all? { |c| guardian.can_see_category? c }
        raise Discourse::InvalidAccess
      end
    end

    by_category.each do |cat, pos|
      cat.position = pos
      cat.save! if cat.will_save_change_to_position?
    end

    render json: success_json
  end

  def types
    guardian.ensure_can_create_category!

    counts_by_type =
      Discourse
        .cache
        .fetch(Categories::TypeRegistry::COUNTS_CACHE_KEY, expires_in: 1.hour) do
          Categories::TypeRegistry.counts
        end

    # NOTE: Keep in mind types may not be visible if their visible? method relies
    # on an upcoming change, and that upcoming change's plugin is not configurable,
    # as is the case with some discourse hosted sites.
    render json: {
             types: Categories::TypeRegistry.list(only_visible: true, guardian:),
             counts: counts_by_type,
           }
  end

  def show
    guardian.ensure_can_see!(@category)

    if Category.topic_create_allowed(guardian).where(id: @category.id).exists?
      @category.permission = CategoryGroup.permission_types[:full]
    end

    render_serialized(@category, CategorySerializer)
  end

  MAX_DESCRIPTION_PARAM_LENGTH = 1000
  def create
    guardian.ensure_can_create!(Category)
    position = category_params.delete(:position)
    category_type = params[:category_type]

    if category_params[:description].present? &&
         category_params[:description].size > MAX_DESCRIPTION_PARAM_LENGTH
      render json: {
               errors: [
                 I18n.t(
                   "category.errors.description_too_long",
                   count: MAX_DESCRIPTION_PARAM_LENGTH,
                 ),
               ],
             },
             status: :unprocessable_entity
      return
    end

    @category =
      begin
        Category.new(required_create_params.merge(user: current_user))
      rescue ArgumentError => e
        return render json: { errors: [e.message] }, status: :unprocessable_entity
      end

    configure_error = nil

    Category.transaction do
      if @category.save
        @category.move_to(position.to_i) if position

        if category_type.present?
          type_class = Categories::TypeRegistry.get(category_type.to_sym)
          allowed_setting_keys = type_class&.configuration_schema_keys(:category_settings) || []
          type_settings = params[:category_type_settings]&.slice(*allowed_setting_keys)&.permit!

          Categories::Configure.call(
            guardian:,
            params: {
              category_id: @category.id,
              category_type:,
              site_setting_configuration_values: params[:category_type_site_settings],
              category_configuration_values: [
                category_params[:custom_fields],
                type_settings,
              ].compact.reduce(:merge),
            },
          ) do
            on_failed_policy(:type_is_available) do
              configure_error = category_type_unavailable_error(category_type)
              raise ActiveRecord::Rollback
            end
          end
        end

        additional_category_types =
          Array(params[:category_types]).compact_blank.map(&:to_sym) -
            [category_type&.to_sym, :discussion].compact

        additional_category_types.each do |additional_category_type|
          Categories::Configure.call(
            guardian:,
            params: {
              category_id: @category.id,
              category_type: additional_category_type,
            },
          ) do
            on_failed_policy(:type_is_available) do
              configure_error = category_type_unavailable_error(additional_category_type)
              raise ActiveRecord::Rollback
            end
          end
        end

        @category.reload if category_type.present? || additional_category_types.any?
      end
    end

    if configure_error
      return render json: { errors: [configure_error] }, status: :unprocessable_entity
    end

    if @category.persisted?
      Scheduler::Defer.later "Log staff action create category" do
        @staff_action_logger.log_category_creation(@category)
      end

      render_serialized(@category, CategorySerializer)
    else
      render_json_error(@category)
    end
  end

  def update
    guardian.ensure_can_edit!(@category)

    json_result(@category, serializer: CategorySerializer) do |cat|
      old_category_params = category_params.dup

      cat.move_to(category_params[:position].to_i) if category_params[:position]
      category_params.delete(:position)

      old_custom_fields = cat.custom_fields.dup
      pending_custom_fields = category_params[:custom_fields]
      category_params.delete(:custom_fields)

      # Handles adding or removing category types registered by plugins
      # based on the multi-type selector in the General tab for categories.
      return nil unless manage_category_types(cat, pending_custom_fields || {})
      cat.reload

      if params[:category_type_settings].present?
        # Re-run configure_category for each matching type so per-type
        # category_settings (e.g. events_calendar_default_view) are persisted on
        # edit, not just on create. Each type only sees its own keys so it
        # cannot be invoked spuriously for other types' settings.
        cat.category_types.each_key do |type_id|
          type_class = Categories::TypeRegistry.get(type_id)
          next unless type_class

          # Skip plugin-enabling types for non-admins
          next if type_class.enables_plugin? && !guardian.is_admin?

          type_values =
            params[:category_type_settings].slice(
              *type_class.configuration_schema_keys(:category_settings),
            )
          next if type_values.empty?

          type_class.configure_category(cat, guardian:, configuration_values: type_values)
        end
      end

      merge_pending_custom_fields!(cat, pending_custom_fields)

      old_permissions = cat.permissions_params
      old_permissions = { Group[:everyone].name => 1 } if old_permissions.empty?

      if result = cat.update(category_params)
        Category.preload_user_fields!(guardian, [cat])

        Scheduler::Defer.later "Log staff action change category settings" do
          @staff_action_logger.log_category_settings_change(
            @category,
            old_category_params,
            old_permissions: old_permissions,
            old_custom_fields: old_custom_fields,
          )
        end
      end

      result
    end
  end

  def convert_nested_replies
    NestedTopic::ConvertCategory.call(
      service_params.deep_merge(params: { category_id: @category.id }),
    ) do
      on_success do |category:, converted_topic_count:|
        render json:
                 success_json.merge(
                   converted_topic_count: converted_topic_count,
                   nested_replies_conversion_completed:
                     category.nested_replies_conversion_completed?,
                 )
      end
      on_failed_contract { raise Discourse::InvalidParameters }
      on_model_not_found(:category) { raise Discourse::NotFound }
      on_failed_policy(:nested_replies_enabled) { raise Discourse::InvalidAccess }
      on_failed_policy(:can_edit_category) { raise Discourse::InvalidAccess }
      on_failed_policy(:category_nested_replies_enabled) { raise Discourse::InvalidAccess }
      on_failure { render json: failed_json, status: :unprocessable_entity }
    end
  end

  def update_slug
    @category = Category.find(params[:category_id].to_i)
    guardian.ensure_can_edit!(@category)

    custom_slug = params[:slug].to_s

    if custom_slug.blank?
      error = @category.errors.full_message(:slug, I18n.t("errors.messages.blank"))
      render_json_error(error)
    elsif @category.update(slug: custom_slug)
      render json: success_json
    else
      render_json_error(@category)
    end
  end

  def set_notifications
    category_id = params[:category_id].to_i
    notification_level = params[:notification_level].to_i

    CategoryUser.set_notification_level_for_category(current_user, notification_level, category_id)
    render json:
             success_json.merge(
               {
                 indirectly_muted_category_ids:
                   CategoryUser.indirectly_muted_category_ids(current_user),
               },
             )
  end

  def destroy
    guardian.ensure_can_delete!(@category)
    @category.destroy
    Discourse.cache.delete(Categories::TypeRegistry::COUNTS_CACHE_KEY)

    Scheduler::Defer.later "Log staff action delete category" do
      @staff_action_logger.log_category_deletion(@category)
    end

    render json: success_json
  end

  def find_by_slug
    params.require(:category_slug)
    @category =
      Category.includes(:category_setting).find_by_slug_path(params[:category_slug].split("/"))

    raise Discourse::NotFound if @category.blank?

    if !guardian.can_see?(@category)
      if SiteSetting.detailed_404 && group = @category.access_category_via_group
        raise Discourse::InvalidAccess.new(
                "not in group",
                @category,
                custom_message: "not_in_group.title_category",
                custom_message_params: {
                  group: group.name,
                },
                group: group,
              )
      else
        raise Discourse::NotFound
      end
    end

    @category.permission = CategoryGroup.permission_types[:full] if Category
      .topic_create_allowed(guardian)
      .where(id: @category.id)
      .exists?
    Category.preload_user_fields!(guardian, [@category])

    render_serialized(@category, CategorySerializer)
  end

  def visible_groups
    @guardian.ensure_can_see!(@category)

    groups =
      if !@category.groups.exists?(id: Group::AUTO_GROUPS[:everyone])
        @category.groups.merge(Group.visible_groups(current_user)).pluck("name")
      end

    render json: success_json.merge(groups: groups || [])
  end

  def find
    categories = []
    serializer = params[:include_permissions] ? CategorySerializer : SiteCategorySerializer

    if params[:ids].present?
      categories = Category.secured(guardian).where(id: params[:ids])
    elsif params[:slug_path].present?
      category = Category.find_by_slug_path(params[:slug_path].split("/"))
      raise Discourse::NotFound if category.blank?
      guardian.ensure_can_see!(category)

      ancestors = Category.secured(guardian).with_ancestors(category.id).where.not(id: category.id)
      categories = [*ancestors, category]
    elsif params[:slug_path_with_id].present?
      category = Category.find_by_slug_path_with_id(params[:slug_path_with_id])
      raise Discourse::NotFound if category.blank?
      guardian.ensure_can_see!(category)

      ancestors = Category.secured(guardian).with_ancestors(category.id).where.not(id: category.id)
      categories = [*ancestors, category]
    end

    raise Discourse::NotFound if categories.blank?

    Category.preload_user_fields!(guardian, categories)

    if serializer == SiteCategorySerializer && Site.preloaded_category_custom_fields.present?
      Category.preload_custom_fields(categories, Site.preloaded_category_custom_fields)
    end

    render_serialized(categories, serializer, root: :categories, scope: guardian)
  end

  def hierarchical_search
    Category::HierarchicalSearch.call(service_params) do
      on_success do |categories:|
        render_json_dump(
          categories: serialize_data(categories, SiteCategorySerializer, scope: guardian),
        )
      end
      on_failed_contract do |contract|
        render json: failed_json.merge(errors: contract.errors.full_messages), status: :bad_request
      end
      on_failure { render json: failed_json, status: :unprocessable_entity }
    end
  end

  def search
    term = params[:term].to_s.strip[0, MAX_CATEGORY_SEARCH_TERM_LENGTH]
    parent_category_id = params[:parent_category_id].to_i if params[:parent_category_id].present?
    include_uncategorized =
      (
        if params[:include_uncategorized].present?
          ActiveModel::Type::Boolean.new.cast(params[:include_uncategorized])
        else
          true
        end
      )
    if params[:select_category_ids].is_a?(Array)
      select_category_ids = params[:select_category_ids].map(&:presence)
    end
    if params[:reject_category_ids].is_a?(Array)
      reject_category_ids = params[:reject_category_ids].map(&:presence)
    end
    include_subcategories =
      if params[:include_subcategories].present?
        ActiveModel::Type::Boolean.new.cast(params[:include_subcategories])
      else
        true
      end
    include_ancestors =
      if params[:include_ancestors].present?
        ActiveModel::Type::Boolean.new.cast(params[:include_ancestors])
      else
        false
      end
    prioritized_category_id = params[:prioritized_category_id].to_i if params[
      :prioritized_category_id
    ].present?
    limit =
      (
        if params[:limit].present?
          params[:limit].to_i.clamp(1, MAX_CATEGORIES_LIMIT)
        else
          MAX_CATEGORIES_LIMIT
        end
      )
    page = [1, params[:page].to_i].max

    categories = Category.secured(guardian)

    if term.present?
      words = []
      term.scan(/\S+/) do |word|
        words << word
        break if words.size >= MAX_CATEGORY_SEARCH_WORDS
      end

      words.each do |word|
        categories =
          categories.where(
            "#{Category.normalize_sql("name")} ILIKE #{Category.normalize_sql("?")}",
            "%#{word}%",
          )
      end
    end

    categories =
      (
        if parent_category_id != -1
          categories.where(parent_category_id: parent_category_id)
        else
          categories.where(parent_category_id: nil)
        end
      ) if parent_category_id.present?

    categories =
      categories.where.not(id: SiteSetting.uncategorized_category_id) if !include_uncategorized

    categories = categories.where(id: select_category_ids) if select_category_ids

    categories = categories.where.not(id: reject_category_ids) if reject_category_ids

    categories = categories.where(parent_category_id: nil) if !include_subcategories

    categories_count = categories.count

    categories =
      categories
        .includes(
          :uploaded_logo,
          :uploaded_logo_dark,
          :uploaded_background,
          :uploaded_background_dark,
          :form_templates,
          category_required_tag_groups: :tag_group,
        )
        .joins("LEFT JOIN topics t on t.id = categories.topic_id")
        .select("categories.*, t.slug topic_slug")
        .order(
          "starts_with(#{Category.normalize_sql("categories.name")}, #{Category.normalize_sql(ActiveRecord::Base.connection.quote(term))}) DESC",
          "categories.parent_category_id IS NULL DESC",
          "categories.id IS NOT DISTINCT FROM #{ActiveRecord::Base.connection.quote(prioritized_category_id)} DESC",
          "categories.parent_category_id IS NOT DISTINCT FROM #{ActiveRecord::Base.connection.quote(prioritized_category_id)} DESC",
          "categories.id ASC",
        )
        .limit(limit)
        .offset((page - 1) * limit)

    if Site.preloaded_category_custom_fields.present?
      Category.preload_custom_fields(categories, Site.preloaded_category_custom_fields)
    end

    Category.preload_user_fields!(guardian, categories)

    response = {
      categories_count: categories_count,
      categories: serialize_data(categories, SiteCategorySerializer, scope: guardian),
    }

    if include_ancestors
      ancestors = Category.secured(guardian).ancestors_of(categories.map(&:id))
      Category.preload_user_fields!(guardian, ancestors)
      response[:ancestors] = serialize_data(ancestors, SiteCategorySerializer, scope: guardian)
    end

    render_json_dump(response)
  end

  private

  def merge_pending_custom_fields!(category, pending_custom_fields)
    pending_custom_fields&.each do |key, value|
      if value.nil? || value == ""
        category.custom_fields.delete(key)
      else
        category.custom_fields[key] = if value.is_a?(TrueClass) || value.is_a?(FalseClass)
          value.to_s
        else
          value
        end
      end
    end
  end

  def category_type_unavailable_error(category_type)
    type_id = category_type.to_sym
    type_class = Categories::TypeRegistry.get(type_id)

    if type_class&.enables_plugin?
      plugin_name = Categories::TypeRegistry.plugin_display_name(type_id)
      I18n.t("category_types.requires_plugin", type_name: category_type.to_s.humanize, plugin_name:)
    else
      I18n.t("category_types.not_available", type_name: category_type.to_s.humanize)
    end
  end

  def manage_category_types(category, pending_custom_fields)
    # NOTE: The code in this block is pretty similar to what we are doing in
    # configure_site_settings in Categories::Types::Base, however here we
    # need to be able to update site settings across all category types for
    # the category and it's best to do this in one query rather than
    # multiple.
    #
    # Maybe in future we do something different, but this is a good starting point.
    if params[:category_type_site_settings].present?
      category_type_settings =
        params[:category_type_site_settings].permit!.to_h.map do |name, value|
          { setting_name: name, value: }
        end

      # We do this because we want to allow updating hidden settings for the
      # category type, but not other settings. The configuration schema for a
      # category type defines which settings it wants to change, so that's a
      # good source to use as an allowlist here.
      allowed_setting_names = category.category_type_site_setting_names
      SiteSetting::Update.call(
        guardian:,
        options: {
          allow_changing_hidden: allowed_setting_names,
        },
        params: {
          settings: category_type_settings,
        },
      )
    end

    if params.has_key?(:category_types)
      # Discussion can never be removed as a category type, so we always add it back.
      new_category_types =
        Array(params[:category_types]).compact_blank.map(&:to_sym) + [:discussion]
      current_category_types = category.category_types.keys
      removed_category_types = current_category_types - new_category_types
      added_category_types = new_category_types - current_category_types

      # Preflight availability check for all added types before any mutations.
      # This prevents partial state changes if a type is unavailable.
      added_category_types.each do |category_type|
        type_class = Categories::TypeRegistry.get(category_type)
        unless type_class&.available_for?(guardian)
          render json: {
                   errors: [category_type_unavailable_error(category_type)],
                 },
                 status: :unprocessable_entity
          return false
        end
      end

      # Some category custom fields (like
      # DiscourseSolved::ENABLE_ACCEPTED_ANSWERS_CUSTOM_FIELD) control whether
      # the type is enabled or not, so we need to remove them from the pending
      # custom fields to avoid turning the type back on/off again.
      (added_category_types + removed_category_types).each do |category_type|
        Categories::TypeRegistry
          .get(category_type)
          .configuration_schema_keys(:category_custom_fields)
          .each { |custom_field_key| pending_custom_fields.delete(custom_field_key) }
      end

      removed_category_types.each do |category_type|
        Categories::Unconfigure.call(
          guardian:,
          params: {
            category_id: category.id,
            category_type:,
          },
        )
      end

      added_category_types.each do |category_type|
        Categories::Configure.call(guardian:, params: { category_id: category.id, category_type: })
      end
    end

    true
  end

  def topics_per_page
    return SiteSetting.categories_topics if SiteSetting.categories_topics > 0

    count = Category.secured(guardian).where(parent_category: nil).count
    count = (SYMMETRICAL_CATEGORIES_TO_TOPICS_FACTOR * count).to_i
    count.clamp(MIN_CATEGORIES_TOPICS, MAX_CATEGORIES_TOPICS)
  end

  def categories_and_topics(topics_filter)
    discourse_expires_in 1.minute

    result = CategoryAndTopicLists.new
    result.category_list = fetch_category_list
    result.topic_list = fetch_topic_list(topics_filter:)

    render_serialized(result, CategoryAndTopicListsSerializer, root: false)
  end

  def required_param_keys
    [:name]
  end

  def required_create_params
    required_param_keys.each { |key| params.require(key) }
    category_params
  end

  def category_params
    @category_params ||=
      begin
        if p = params[:permissions]
          p.each { |k, v| p[k] = v.to_i }
        end

        if SiteSetting.tagging_enabled
          params[:allowed_tags] = params[:allowed_tags].presence || [] if params[:allowed_tags]
          params[:allowed_tag_groups] = params[:allowed_tag_groups].presence || [] if params[
            :allowed_tag_groups
          ]
          params[:required_tag_groups] = params[:required_tag_groups].presence || [] if params[
            :required_tag_groups
          ]
        end

        conditional_param_keys = []
        if SiteSetting.enable_category_group_moderation?
          conditional_param_keys << { moderating_group_ids: [] }
        end

        if SiteSetting.content_localization_enabled?
          conditional_param_keys << :locale
          conditional_param_keys << { category_localizations: %i[id locale name description] }
        end

        permitted_params = [
          *required_param_keys,
          :position,
          :name,
          :color,
          :text_color,
          :style_type,
          :emoji,
          :icon,
          :email_in,
          :email_in_allow_strangers,
          :mailinglist_mirror,
          :all_topics_wiki,
          :allow_unlimited_owner_edits_on_first_post,
          :default_slow_mode_seconds,
          :parent_category_id,
          :auto_close_hours,
          :auto_close_based_on_last_post,
          :uploaded_logo_id,
          :uploaded_logo_dark_id,
          :uploaded_background_id,
          :uploaded_background_dark_id,
          :slug,
          :allow_badges,
          :topic_template,
          :topic_title_placeholder,
          :description,
          :sort_order,
          :sort_ascending,
          :topic_featured_link_allowed,
          :show_subcategory_list,
          :num_featured_topics,
          :default_view,
          :subcategory_list_style,
          :default_top_period,
          :minimum_required_tags,
          :navigate_to_first_post_after_read,
          :search_priority,
          :allow_global_tags,
          :read_only_banner,
          :default_list_filter,
          { topic_posting_review_group_ids: [] },
          { reply_posting_review_group_ids: [] },
          *conditional_param_keys,
        ]

        DiscoursePluginRegistry.category_update_param_with_callback.each do |param_name, config|
          permitted_params << param_name if config[:plugin].enabled?
        end

        result =
          params.permit(
            *permitted_params,
            category_setting_attributes: %i[
              auto_bump_cooldown_days
              num_auto_bump_daily
              require_reply_approval
              require_topic_approval
              nested_replies_default
              topic_posting_review_mode
              reply_posting_review_mode
            ],
            custom_fields: {
            },
            permissions: [*p.try(:keys)],
            allowed_tags: [],
            allowed_tag_groups: [],
            required_tag_groups: %i[name min_count],
            form_template_ids: [],
          )

        if result[:required_tag_groups] && !result[:required_tag_groups].is_a?(Array)
          raise Discourse::InvalidParameters.new(:required_tag_groups)
        end

        if @category
          DiscoursePluginRegistry.category_update_param_with_callback.each do |param_name, config|
            next if !config[:plugin].enabled?
            next if !result.key?(param_name)

            @category.instance_variable_set(:"@#{param_name}_callback_value", result[param_name])
            # remove from params so that AR doesn't try to set it as an attribute
            result.delete(param_name)
          end
        end

        result
      end
  end

  def fetch_category
    @category = Category.find_by_slug(params[:id]) || Category.find_by(id: params[:id].to_i)
    raise Discourse::NotFound if @category.blank?
  end

  def fetch_category_list
    parent_category =
      if params[:parent_category_id].present?
        Category.find_by_slug(params[:parent_category_id]) ||
          Category.find_by(id: params[:parent_category_id].to_i)
      elsif params[:category_slug_path_with_id].present?
        Category.find_by_slug_path_with_id(params[:category_slug_path_with_id])
      end

    include_topics =
      params[:include_topics] ||
        (parent_category && parent_category.subcategory_list_includes_topics?) ||
        SiteSetting.desktop_category_page_style == "categories_with_featured_topics" ||
        SiteSetting.desktop_category_page_style == "subcategories_with_featured_topics" ||
        SiteSetting.desktop_category_page_style == "categories_boxes_with_topics" ||
        SiteSetting.desktop_category_page_style == "categories_with_top_topics" ||
        SiteSetting.mobile_category_page_style == "categories_with_featured_topics" ||
        SiteSetting.mobile_category_page_style == "categories_boxes_with_topics" ||
        SiteSetting.mobile_category_page_style == "subcategories_with_featured_topics"

    include_subcategories =
      SiteSetting.desktop_category_page_style == "subcategories_with_featured_topics" ||
        SiteSetting.mobile_category_page_style == "subcategories_with_featured_topics" ||
        params[:include_subcategories] == "true"

    category_options = {
      is_homepage: current_homepage == "categories",
      parent_category_id: parent_category&.id,
      include_topics: include_topics,
      include_subcategories: include_subcategories,
      tag: params[:tag],
      page: params[:page].try(:to_i) || 1,
    }

    @category_list = CategoryList.new(guardian, category_options)
  end

  # The crawler layout renders categories only and emits no preloaded data, so
  # building and serializing the list would be pure waste.
  def preload_topic_list
    return if use_crawler_layout?

    @topic_list = fetch_topic_list
    return if @topic_list.blank? || @topic_list.topics.blank?

    store_preloaded(
      @topic_list.preload_key,
      MultiJson.dump(TopicListSerializer.new(@topic_list, scope: guardian)),
    )
  end

  def fetch_topic_list(topics_filter: nil)
    style =
      if topics_filter
        "categories_and_#{topics_filter}_topics"
      else
        SiteSetting.desktop_category_page_style
      end

    topic_options = { per_page: topics_per_page, no_definitions: true }
    topic_options.merge!(build_topic_list_options)
    topic_options[:order] = "created" if SiteSetting.desktop_category_page_style ==
      "categories_and_latest_topics_created_date"

    case style
    when "categories_and_latest_topics", "categories_and_latest_topics_created_date"
      @topic_list = TopicQuery.new(current_user, topic_options).list_latest
      @topic_list.more_topics_url = url_for(latest_path(sort: topic_options[:order]))
    when "categories_and_top_topics"
      @topic_list =
        TopicQuery.new(current_user, topic_options).list_top_for(
          SiteSetting.top_page_default_timeframe.to_sym,
        )
      @topic_list.more_topics_url = url_for(top_path)
    when "categories_and_hot_topics"
      @topic_list = TopicQuery.new(current_user, topic_options).list_hot
      @topic_list.more_topics_url = url_for(hot_path)
    end

    @topic_list
  end

  def initialize_staff_action_logger
    @staff_action_logger = StaffActionLogger.new(current_user)
  end
end
