# frozen_string_literal: true

class PostsController < ApplicationController
  # Bug with Rails 7+
  # see https://github.com/rails/rails/issues/44867
  self._flash_types -= [:notice]

  requires_login except: %i[
                   show
                   replies
                   by_number
                   by_date
                   short_link
                   reply_history
                   reply_ids
                   revisions
                   latest_revision
                   expand_embed
                   markdown_id
                   markdown_num
                   cooked
                   latest
                   user_posts_feed
                 ]

  skip_before_action :preload_json,
                     :check_xhr,
                     only: %i[markdown_id markdown_num short_link latest user_posts_feed]

  MARKDOWN_TOPIC_PAGE_SIZE ||= 100

  def markdown_id
    markdown Post.find_by(id: params[:id].to_i)
  end

  def markdown_num
    if params[:revision].present?
      post_revision = find_post_revision_from_topic_id
      render plain: post_revision.modifications[:raw].last
    elsif params[:post_number].present?
      markdown Post.find_by(
                 topic_id: params[:topic_id].to_i,
                 post_number: params[:post_number].to_i,
               )
    else
      opts = params.slice(:page)
      opts[:limit] = MARKDOWN_TOPIC_PAGE_SIZE
      topic_view = TopicView.new(params[:topic_id], current_user, opts)
      content = topic_view.posts.map { |p| <<~MD }
          #{p.user.username} | #{p.updated_at} | ##{p.post_number}

          #{p.raw}

          -------------------------

        MD
      render plain: content.join
    end
  end

  def latest
    params.permit(:before)
    last_post_id = params[:before].to_i
    last_post_id = Post.last.id if last_post_id <= 0

    if params[:id] == "private_posts"
      raise Discourse::NotFound if current_user.nil?
      posts =
        Post
          .private_posts
          .order(created_at: :desc)
          .where("posts.id <= ?", last_post_id)
          .where("posts.id > ?", last_post_id - 50)
          .includes(topic: :category)
          .includes(user: %i[primary_group flair_group])
          .includes(:reply_to_user)
          .limit(50)
      rss_description = I18n.t("rss_description.private_posts")
    else
      posts =
        Post
          .public_posts
          .visible
          .where(post_type: Post.types[:regular])
          .order(created_at: :desc)
          .where("posts.id <= ?", last_post_id)
          .where("posts.id > ?", last_post_id - 50)
          .includes(topic: :category)
          .includes(user: %i[primary_group flair_group])
          .includes(:reply_to_user)
          .limit(50)
      rss_description = I18n.t("rss_description.posts")
      @use_canonical = true
    end

    # Remove posts the user doesn't have permission to see
    # This isn't leaking any information we weren't already through the post ID numbers
    posts = posts.reject { |post| !guardian.can_see?(post) || post.topic.blank? }
    counts = PostAction.counts_for(posts, current_user)

    respond_to do |format|
      format.rss do
        @posts = posts
        @title = "#{SiteSetting.title} - #{rss_description}"
        @link = Discourse.base_url
        @description = rss_description
        render "posts/latest", formats: [:rss]
      end
      format.json do
        render_json_dump(
          serialize_data(
            posts,
            PostSerializer,
            scope: guardian,
            root: params[:id],
            add_raw: true,
            add_title: true,
            all_post_actions: counts,
          ),
        )
      end
    end
  end

  def user_posts_feed
    params.require(:username)
    user = fetch_user_from_params
    raise Discourse::NotFound unless guardian.can_see_profile?(user)

    posts =
      Post
        .public_posts
        .visible
        .where(user_id: user.id)
        .where(post_type: Post.types[:regular])
        .order(created_at: :desc)
        .includes(:user)
        .includes(topic: :category)
        .limit(50)

    posts = posts.reject { |post| !guardian.can_see?(post) || post.topic.blank? }

    respond_to do |format|
      format.rss do
        @posts = posts
        @title =
          "#{SiteSetting.title} - #{I18n.t("rss_description.user_posts", username: user.username)}"
        @link = "#{user.full_url}/activity"
        @description = I18n.t("rss_description.user_posts", username: user.username)
        render "posts/latest", formats: [:rss]
      end

      format.json do
        render_json_dump(serialize_data(posts, PostSerializer, scope: guardian, add_excerpt: true))
      end
    end
  end

  def cooked
    render json: { cooked: find_post_from_params.cooked }
  end

  def raw_email
    params.require(:id)
    post = Post.unscoped.find(params[:id].to_i)
    guardian.ensure_can_view_raw_email!(post)
    text, html = Email.extract_parts(post.raw_email)
    render json: { raw_email: post.raw_email, text_part: text, html_part: html }
  end

  def short_link
    post = Post.find_by(id: params[:post_id].to_i)
    raise Discourse::NotFound unless post

    # Stuff the user in the request object, because that's what IncomingLink wants
    if params[:user_id]
      user = User.find_by(id: params[:user_id].to_i)
      request["u"] = user.username_lower if user
    end

    guardian.ensure_can_see!(post)
    redirect_to path(post.url)
  end

  def create
    manager_params = create_params
    manager_params[:first_post_checks] = !is_api?
    manager_params[:advance_draft] = !is_api?

    manager = NewPostManager.new(current_user, manager_params)

    json =
      if is_api?
        memoized_payload =
          DistributedMemoizer.memoize(signature_for(manager_params), 120) do
            MultiJson.dump(serialize_data(manager.perform, NewPostResultSerializer, root: false))
          end

        JSON.parse(memoized_payload)
      else
        serialize_data(manager.perform, NewPostResultSerializer, root: false)
      end

    backwards_compatible_json(json)
  end

  def update
    params.require(:post)

    post = Post.where(id: params[:id])
    post = post.with_deleted if guardian.is_staff?
    post = post.first

    raise Discourse::NotFound if post.blank?

    post.image_sizes = params[:image_sizes] if params[:image_sizes].present?

    if !guardian.public_send("can_edit?", post) && post.user_id == current_user.id &&
         post.edit_time_limit_expired?(current_user)
      return render_json_error(I18n.t("too_late_to_edit"))
    end

    guardian.ensure_can_edit!(post)

    changes = { raw: params[:post][:raw], edit_reason: params[:post][:edit_reason] }

    Post.plugin_permitted_update_params.keys.each { |param| changes[param] = params[:post][param] }

    raw_old = params[:post][:raw_old]
    if raw_old.present? && raw_old != post.raw
      return render_json_error(I18n.t("edit_conflict"), status: 409)
    end

    # to stay consistent with the create api, we allow for title & category changes here
    if post.is_first_post?
      changes[:title] = params[:title] if params[:title]
      changes[:category_id] = params[:post][:category_id] if params[:post][:category_id]

      if changes[:category_id] && changes[:category_id].to_i != post.topic.category_id.to_i
        category = Category.find_by(id: changes[:category_id])
        if category || (changes[:category_id].to_i == 0)
          guardian.ensure_can_move_topic_to_category!(category)
        else
          return render_json_error(I18n.t("category.errors.not_found"))
        end
      end
    end

    # We don't need to validate edits to small action posts by staff
    opts = {}
    if post.post_type == Post.types[:small_action] && current_user.staff?
      opts[:skip_validations] = true
    end

    topic = post.topic
    topic = Topic.with_deleted.find(post.topic_id) if guardian.is_staff?

    revisor = PostRevisor.new(post, topic)
    revisor.revise!(current_user, changes, opts)

    return render_json_error(post) if post.errors.present?
    return render_json_error(topic) if topic.errors.present?

    post_serializer = PostSerializer.new(post, scope: guardian, root: false, add_raw: true)
    post_serializer.draft_sequence = DraftSequence.current(current_user, topic.draft_key)
    link_counts = TopicLink.counts_for(guardian, topic, [post])
    post_serializer.single_post_link_counts = link_counts[post.id] if link_counts.present?

    result = { post: post_serializer.as_json }
    if revisor.category_changed.present?
      result[:category] = BasicCategorySerializer.new(
        revisor.category_changed,
        scope: guardian,
        root: false,
      ).as_json
    end

    render_json_dump(result)
  end

  def show
    post = find_post_from_params
    display_post(post)
  end

  def by_number
    post = find_post_from_params_by_number
    display_post(post)
  end

  def by_date
    post = find_post_from_params_by_date
    display_post(post)
  end

  def reply_history
    post = find_post_from_params

    reply_history = post.reply_history(params[:max_replies].to_i, guardian)
    user_custom_fields = {}
    if (added_fields = User.allowed_user_custom_fields(guardian)).present?
      user_custom_fields = User.custom_fields_for_ids(reply_history.pluck(:user_id), added_fields)
    end

    render_serialized(reply_history, PostSerializer, user_custom_fields: user_custom_fields)
  end

  def reply_ids
    post = find_post_from_params
    render json: post.reply_ids(guardian).to_json
  end

  def destroy
    post = find_post_from_params
    force_destroy = ActiveModel::Type::Boolean.new.cast(params[:force_destroy])

    if force_destroy
      if !guardian.can_permanently_delete?(post)
        return render_json_error post.cannot_permanently_delete_reason(current_user), status: 403
      end
    else
      guardian.ensure_can_delete!(post)
    end

    unless guardian.can_moderate_topic?(post.topic)
      RateLimiter.new(
        current_user,
        "delete_post_per_min",
        SiteSetting.max_post_deletions_per_minute,
        1.minute,
      ).performed!
      RateLimiter.new(
        current_user,
        "delete_post_per_day",
        SiteSetting.max_post_deletions_per_day,
        1.day,
      ).performed!
    end

    PostDestroyer.new(
      current_user,
      post,
      context: params[:context],
      force_destroy: force_destroy,
    ).destroy

    render body: nil
  end

  def expand_embed
    render json: { cooked: TopicEmbed.expanded_for(find_post_from_params) }
  rescue StandardError
    render_json_error I18n.t("errors.embed.load_from_remote")
  end

  def recover
    post = find_post_from_params
    guardian.ensure_can_recover_post!(post)

    unless guardian.can_moderate_topic?(post.topic)
      RateLimiter.new(
        current_user,
        "delete_post_per_min",
        SiteSetting.max_post_deletions_per_minute,
        1.minute,
      ).performed!
      RateLimiter.new(
        current_user,
        "delete_post_per_day",
        SiteSetting.max_post_deletions_per_day,
        1.day,
      ).performed!
    end

    destroyer = PostDestroyer.new(current_user, post)
    destroyer.recover
    post.reload

    render_post_json(post)
  end

  def destroy_many
    params.require(:post_ids)
    agree_with_first_reply_flag = (params[:agree_with_first_reply_flag] || true).to_s == "true"

    posts = Post.where(id: post_ids_including_replies).order(:id)
    raise Discourse::InvalidParameters.new(:post_ids) if posts.blank?

    # Make sure we can delete the posts
    posts.each { |p| guardian.ensure_can_delete!(p) }

    Post.transaction do
      posts.each_with_index do |p, i|
        PostDestroyer.new(
          current_user,
          p,
          defer_flags: !(agree_with_first_reply_flag && i == 0),
        ).destroy
      end
    end

    render body: nil
  end

  def merge_posts
    params.require(:post_ids)
    posts = Post.where(id: params[:post_ids]).order(:id)
    raise Discourse::InvalidParameters.new(:post_ids) if posts.pluck(:id) == params[:post_ids]
    PostMerger.new(current_user, posts).merge
    render body: nil
  rescue PostMerger::CannotMergeError => e
    render_json_error(e.message)
  end

  # Direct replies to this post
  def replies
    post = find_post_from_params
    replies = post.replies.secured(guardian)

    user_custom_fields = {}
    if (added_fields = User.allowed_user_custom_fields(guardian)).present?
      user_custom_fields = User.custom_fields_for_ids(replies.pluck(:user_id), added_fields)
    end

    render_serialized(replies, PostSerializer, user_custom_fields: user_custom_fields)
  end

  def revisions
    post = find_post_from_params
    raise Discourse::NotFound if post.hidden && !guardian.can_view_hidden_post_revisions?

    post_revision = find_post_revision_from_params
    post_revision_serializer =
      PostRevisionSerializer.new(post_revision, scope: guardian, root: false)
    render_json_dump(post_revision_serializer)
  end

  def latest_revision
    post = find_post_from_params
    raise Discourse::NotFound if post.hidden && !guardian.can_view_hidden_post_revisions?

    post_revision = find_latest_post_revision_from_params
    post_revision_serializer =
      PostRevisionSerializer.new(post_revision, scope: guardian, root: false)
    render_json_dump(post_revision_serializer)
  end

  def hide_revision
    post_revision = find_post_revision_from_params
    guardian.ensure_can_hide_post_revision!(post_revision)

    post_revision.hide!

    post = find_post_from_params
    post.public_version -= 1
    post.save

    render body: nil
  end

  def permanently_delete_revisions
    guardian.ensure_can_permanently_delete_post_revisions!

    post = find_post_from_params
    raise Discourse::InvalidParameters.new(:post) if post.blank?
    raise Discourse::NotFound unless post.revisions.present?

    RateLimiter.new(
      current_user,
      "admin_permanently_delete_post_revisions",
      20,
      1.minute,
      apply_limit_to_staff: true,
    ).performed!

    ActiveRecord::Base.transaction do
      updated_at = Time.zone.now
      post.revisions.destroy_all
      post.update(version: 1, public_version: 1, last_version_at: updated_at)
      StaffActionLogger.new(current_user).log_permanently_delete_post_revisions(post)
    end

    post.rebake!

    render body: nil
  end

  def show_revision
    post_revision = find_post_revision_from_params
    guardian.ensure_can_show_post_revision!(post_revision)

    post_revision.show!

    post = find_post_from_params
    post.public_version += 1
    post.save

    render body: nil
  end

  def revert
    raise Discourse::NotFound unless guardian.is_staff?

    post_id = params[:id] || params[:post_id]
    revision = params[:revision].to_i
    raise Discourse::InvalidParameters.new(:revision) if revision < 2

    post_revision = PostRevision.find_by(post_id: post_id, number: revision)
    raise Discourse::NotFound unless post_revision

    post = find_post_from_params
    raise Discourse::NotFound if post.blank?

    post_revision.post = post
    guardian.ensure_can_see!(post_revision)
    guardian.ensure_can_edit!(post)
    if post_revision.modifications["raw"].blank? && post_revision.modifications["title"].blank? &&
         post_revision.modifications["category_id"].blank?
      return render_json_error(I18n.t("revert_version_same"))
    end

    topic = Topic.with_deleted.find(post.topic_id)

    changes = {}
    changes[:raw] = post_revision.modifications["raw"][0] if post_revision.modifications[
      "raw"
    ].present? && post_revision.modifications["raw"][0] != post.raw
    if post.is_first_post?
      changes[:title] = post_revision.modifications["title"][0] if post_revision.modifications[
        "title"
      ].present? && post_revision.modifications["title"][0] != topic.title
      changes[:category_id] = post_revision.modifications["category_id"][
        0
      ] if post_revision.modifications["category_id"].present? &&
        post_revision.modifications["category_id"][0] != topic.category.id
    end
    return render_json_error(I18n.t("revert_version_same")) if changes.length <= 0
    changes[:edit_reason] = I18n.with_locale(SiteSetting.default_locale) do
      I18n.t("reverted_to_version", version: post_revision.number.to_i - 1)
    end

    revisor = PostRevisor.new(post, topic)
    revisor.revise!(current_user, changes)

    return render_json_error(post) if post.errors.present?
    return render_json_error(topic) if topic.errors.present?

    post_serializer = PostSerializer.new(post, scope: guardian, root: false)
    post_serializer.draft_sequence = DraftSequence.current(current_user, topic.draft_key)

    link_counts = TopicLink.counts_for(guardian, topic, [post])
    post_serializer.single_post_link_counts = link_counts[post.id] if link_counts.present?

    result = { post: post_serializer.as_json }
    if post.is_first_post?
      result[:topic] = BasicTopicSerializer.new(
        topic,
        scope: guardian,
        root: false,
      ).as_json if post_revision.modifications["title"].present?
      result[:category_id] = post_revision.modifications["category_id"][
        0
      ] if post_revision.modifications["category_id"].present?
    end

    render_json_dump(result)
  end

  def locked
    post = find_post_from_params
    locker = PostLocker.new(post, current_user)
    params[:locked] === "true" ? locker.lock : locker.unlock
    render_json_dump(locked: post.locked?)
  end

  def notice
    post = find_post_from_params
    raise Discourse::NotFound unless guardian.can_edit_staff_notes?(post.topic)

    old_notice = post.custom_fields[Post::NOTICE]

    if params[:notice].present?
      post.custom_fields[Post::NOTICE] = {
        type: Post.notices[:custom],
        raw: params[:notice],
        cooked: PrettyText.cook(params[:notice], features: { onebox: false }),
      }
    else
      post.custom_fields.delete(Post::NOTICE)
    end

    post.save_custom_fields

    StaffActionLogger.new(current_user).log_post_staff_note(
      post,
      old_value: old_notice&.[]("raw"),
      new_value: params[:notice],
    )

    render body: nil
  end

  def destroy_bookmark
    params.require(:post_id)

    bookmark_id =
      Bookmark.where(
        bookmarkable_id: params[:post_id],
        bookmarkable_type: "Post",
        user_id: current_user.id,
      ).pick(:id)
    destroyed_bookmark = BookmarkManager.new(current_user).destroy(bookmark_id)

    render json:
             success_json.merge(BookmarkManager.bookmark_metadata(destroyed_bookmark, current_user))
  end

  def wiki
    post = find_post_from_params
    params.require(:wiki)
    guardian.ensure_can_wiki!(post)

    post.revise(current_user, wiki: params[:wiki])

    render body: nil
  end

  def post_type
    guardian.ensure_can_change_post_type!
    post = find_post_from_params
    params.require(:post_type)
    raise Discourse::InvalidParameters.new(:post_type) if Post.types[params[:post_type].to_i].blank?

    post.revise(current_user, post_type: params[:post_type].to_i)

    render body: nil
  end

  def rebake
    guardian.ensure_can_rebake!

    post = find_post_from_params
    post.rebake!(invalidate_oneboxes: true, invalidate_broken_images: true)

    render body: nil
  end

  def unhide
    post = find_post_from_params

    guardian.ensure_can_unhide!(post)

    post.unhide!

    render body: nil
  end

  DELETED_POSTS_MAX_LIMIT = 100

  def deleted_posts
    params.permit(:offset, :limit)
    guardian.ensure_can_see_deleted_posts!

    user = fetch_user_from_params
    offset = [params[:offset].to_i, 0].max
    limit = fetch_limit_from_params(default: 60, max: DELETED_POSTS_MAX_LIMIT)

    posts = user_posts(guardian, user.id, offset: offset, limit: limit).where.not(deleted_at: nil)

    render_serialized(posts, AdminUserActionSerializer)
  end

  def pending
    params.require(:username)
    user = fetch_user_from_params
    raise Discourse::NotFound unless guardian.can_edit_user?(user)

    render_serialized(
      user.pending_posts.order(created_at: :desc),
      PendingPostSerializer,
      root: :pending_posts,
    )
  end

  protected

  def markdown(post)
    if post && guardian.can_see?(post)
      render plain: post.raw
    else
      raise Discourse::NotFound
    end
  end

  # We can't break the API for making posts. The new, queue supporting API
  # doesn't return the post as the root JSON object, but as a nested object.
  # If a param is present it uses that result structure.
  def backwards_compatible_json(json_obj)
    json_obj.symbolize_keys!

    success = json_obj[:success]

    if params[:nested_post].blank? && json_obj[:errors].blank? &&
         json_obj[:action].to_s != "enqueued"
      json_obj = json_obj[:post]
    end

    if !success && GlobalSetting.try(:verbose_api_logging) && (is_api? || is_user_api?)
      Rails.logger.error "Error creating post via API:\n\n#{json_obj.inspect}"
    end

    render json: json_obj, status: (!!success) ? 200 : 422
  end

  def find_post_revision_from_params
    post_id = params[:id] || params[:post_id]
    revision = params[:revision].to_i
    raise Discourse::InvalidParameters.new(:revision) if revision < 2

    post_revision = PostRevision.find_by(post_id: post_id, number: revision)
    raise Discourse::NotFound unless post_revision

    post_revision.post = find_post_from_params
    guardian.ensure_can_see!(post_revision)

    post_revision
  end

  def find_latest_post_revision_from_params
    post_id = params[:id] || params[:post_id]

    finder = PostRevision.where(post_id: post_id).order(:number)
    finder = finder.where(hidden: false) unless guardian.is_staff?
    post_revision = finder.last

    raise Discourse::NotFound unless post_revision

    post_revision.post = find_post_from_params
    guardian.ensure_can_see!(post_revision)

    post_revision
  end

  def find_post_revision_from_topic_id
    post =
      Post.find_by(topic_id: params[:topic_id].to_i, post_number: (params[:post_number] || 1).to_i)
    raise Discourse::NotFound unless guardian.can_see?(post)

    revision = params[:revision].to_i
    raise Discourse::NotFound if revision < 2

    post_revision = PostRevision.find_by(post_id: post.id, number: revision)
    raise Discourse::NotFound unless post_revision

    post_revision.post = post
    guardian.ensure_can_see!(post_revision)

    post_revision
  end

  private

  def user_posts(guardian, user_id, opts)
    # Topic.unscoped is necessary to remove the default deleted_at: nil scope
    posts =
      Topic.unscoped do
        Post
          .includes(:user, :topic, :deleted_by, :user_actions)
          .where(user_id: user_id)
          .with_deleted
          .order(created_at: :desc)
      end

    if guardian.user.moderator?
      # Awful hack, but you can't seem to remove the `default_scope` when joining
      # So instead I grab the topics separately
      topic_ids = posts.dup.pluck(:topic_id)
      topics = Topic.where(id: topic_ids).with_deleted.where.not(archetype: "private_message")
      topics = topics.secured(guardian)

      posts = posts.where(topic_id: topics)
    end

    posts.offset(opts[:offset]).limit(opts[:limit])
  end

  def create_params
    permitted = %i[
      raw
      topic_id
      archetype
      category
      target_recipients
      reply_to_post_number
      auto_track
      typing_duration_msecs
      composer_open_duration_msecs
      visible
      draft_key
    ]

    Post.plugin_permitted_create_params.each do |key, value|
      if value[:plugin].enabled?
        permitted << case value[:type]
        when :string
          key.to_sym
        when :array
          { key => [] }
        when :hash
          { key => {} }
        end
      end
    end

    # param munging for WordPress
    params[:auto_track] = !(params[:auto_track].to_s == "false") if params[:auto_track]
    params[:visible] = (params[:unlist_topic].to_s == "false") if params[:unlist_topic]

    if is_api?
      # php seems to be sending this incorrectly, don't fight with it
      params[:skip_validations] = params[:skip_validations].to_s == "true"
      permitted << :skip_validations

      params[:import_mode] = params[:import_mode].to_s == "true"
      permitted << :import_mode

      # We allow `embed_url` via the API
      permitted << :embed_url

      # We allow `created_at` via the API
      permitted << :created_at

      # We allow `external_id` via the API
      permitted << :external_id
    end

    result =
      params
        .permit(*permitted)
        .tap do |allowed|
          allowed[:image_sizes] = params[:image_sizes]

          if params.has_key?(:meta_data)
            Discourse.deprecate(
              "the :meta_data param is deprecated, use the :topic_custom_fields param instead",
              since: "3.2",
              drop_from: "3.3",
            )
          end

          topic_custom_fields = {}
          topic_custom_fields.merge!(editable_topic_custom_fields(:meta_data))
          topic_custom_fields.merge!(editable_topic_custom_fields(:topic_custom_fields))

          if topic_custom_fields.present?
            allowed[:topic_opts] = { custom_fields: topic_custom_fields }
          end
        end

    # Staff are allowed to pass `is_warning`
    if current_user.staff?
      params.permit(:is_warning)
      result[:is_warning] = (params[:is_warning] == "true")
    else
      result[:is_warning] = false
    end

    if params[:no_bump] == "true"
      raise Discourse::InvalidParameters.new(:no_bump) unless guardian.can_skip_bump?
      result[:no_bump] = true
    end

    if params[:shared_draft] == "true"
      raise Discourse::InvalidParameters.new(:shared_draft) unless guardian.can_create_shared_draft?

      result[:shared_draft] = true
    end

    if params[:whisper] == "true"
      unless guardian.can_create_whisper?
        raise Discourse::InvalidAccess.new(
                "invalid_whisper_access",
                nil,
                custom_message: "invalid_whisper_access",
              )
      end

      result[:post_type] = Post.types[:whisper]
    end

    PostRevisor.tracked_topic_fields.each_key do |f|
      params.permit(f => [])
      result[f] = params[f] if params.has_key?(f)
    end

    # Stuff we can use in spam prevention plugins
    result[:ip_address] = request.remote_ip
    result[:user_agent] = request.user_agent
    result[:referrer] = request.env["HTTP_REFERER"]

    recipients = result[:target_recipients]

    if recipients
      recipients = recipients.split(",").map(&:downcase)
      groups =
        Group.messageable(current_user).where("lower(name) in (?)", recipients).pluck("lower(name)")
      recipients -= groups
      emails = recipients.select { |user| user.match(/@/) }
      recipients -= emails
      result[:target_usernames] = recipients.join(",")
      result[:target_emails] = emails.join(",")
      result[:target_group_names] = groups.join(",")
    end

    result.permit!
    result.to_h
  end

  def editable_topic_custom_fields(params_key)
    if (topic_custom_fields = params[params_key]).present?
      editable_topic_custom_fields = Topic.editable_custom_fields(guardian)

      if (
           unpermitted_topic_custom_fields =
             topic_custom_fields.except(*editable_topic_custom_fields)
         ).present?
        raise Discourse::InvalidParameters.new(
                "The following keys in :#{params_key} are not permitted: #{unpermitted_topic_custom_fields.keys.join(", ")}",
              )
      end

      topic_custom_fields.permit(*editable_topic_custom_fields).to_h
    else
      {}
    end
  end

  def signature_for(args)
    +"post##" << Digest::SHA1.hexdigest(
      args
        .to_h
        .to_a
        .concat([["user", current_user.id]])
        .sort { |x, y| x[0] <=> y[0] }
        .join { |x, y| "#{x}:#{y}" },
    )
  end

  def display_post(post)
    post.revert_to(params[:version].to_i) if params[:version].present?
    render_post_json(post)
  end

  def find_post_from_params
    by_id_finder = Post.where(id: params[:id] || params[:post_id])
    find_post_using(by_id_finder)
  end

  def find_post_from_params_by_number
    by_number_finder = Post.where(topic_id: params[:topic_id], post_number: params[:post_number])
    find_post_using(by_number_finder)
  end

  def find_post_from_params_by_date
    by_date_finder =
      TopicView
        .new(params[:topic_id], current_user)
        .filtered_posts
        .where("created_at >= ?", Time.zone.parse(params[:date]))
        .order("created_at ASC")
        .limit(1)

    find_post_using(by_date_finder)
  end

  def find_post_using(finder)
    # A deleted post can be seen by staff or a category group moderator for the topic.
    # But we must find the deleted post to determine which category it belongs to, so
    # we must find.with_deleted
    post = finder.with_deleted.first
    raise Discourse::NotFound unless post

    post.topic = Topic.with_deleted.find_by(id: post.topic_id)

    if !post.topic ||
         (
           (post.deleted_at.present? || post.topic.deleted_at.present?) &&
             !guardian.can_moderate_topic?(post.topic)
         )
      raise Discourse::NotFound
    end

    guardian.ensure_can_see!(post)
    post
  end
end