# frozen_string_literal: true

class DraftsController < ApplicationController
  requires_login

  skip_before_action :check_xhr, :preload_json

  INDEX_LIMIT = 50
  BULK_DESTROY_LIMIT = 30

  def index
    params.permit(:offset)

    stream =
      Draft.stream(
        user: current_user,
        offset: params[:offset],
        limit: fetch_limit_from_params(default: nil, max: INDEX_LIMIT),
      )

    response = { drafts: serialize_data(stream, DraftSerializer) }

    if guardian.can_lazy_load_categories?
      category_ids = stream.map { |draft| draft.topic&.category_id }.compact.uniq
      categories = Category.secured(guardian).with_parents(category_ids)
      response[:categories] = serialize_data(categories, CategoryBadgeSerializer)
    end

    render json: response
  end

  def show
    raise Discourse::NotFound.new if params[:id].blank?

    seq = params[:sequence] || DraftSequence.current(current_user, params[:id])
    render json: { draft: Draft.get(current_user, params[:id], seq), draft_sequence: seq }
  end

  def create
    raise Discourse::NotFound.new if params[:draft_key].blank?

    if !params[:data].is_a?(String) || params[:data].size > SiteSetting.max_draft_length
      raise Discourse::InvalidParameters.new(:data)
    end

    begin
      data = JSON.parse(params[:data])
    rescue JSON::ParserError
      raise Discourse::InvalidParameters.new(:data)
    end

    if reached_max_drafts_per_user?(params)
      render_json_error I18n.t("draft.too_many_drafts.title"),
                        status: 403,
                        extras: {
                          description:
                            I18n.t(
                              "draft.too_many_drafts.description",
                              base_url: Discourse.base_url,
                            ),
                        }
      return
    end

    sequence =
      begin
        Draft.set(
          current_user,
          params[:draft_key],
          params[:sequence].to_i,
          params[:data],
          params[:owner],
          force_save: params[:force_save],
        )
      rescue Draft::OutOfSequence
        begin
          if !Draft.exists?(user_id: current_user.id, draft_key: params[:draft_key])
            Draft.set(
              current_user,
              params[:draft_key],
              DraftSequence.current(current_user, params[:draft_key]),
              params[:data],
              params[:owner],
            )
          else
            raise Draft::OutOfSequence
          end
        rescue Draft::OutOfSequence
          render_json_error I18n.t("draft.sequence_conflict_error.title"),
                            status: 409,
                            extras: {
                              description: I18n.t("draft.sequence_conflict_error.description"),
                            }
          return
        end
      end

    json = success_json.merge(draft_sequence: sequence)

    # check for conflicts when editing a post
    if data.present? && data["postId"].present? && data["action"].to_s.start_with?("edit")
      original_text = data["original_text"] || data["originalText"]
      original_title = data["original_title"]
      original_tags = data["original_tags"]

      if original_text.present?
        if post = Draft.allowed_draft_posts_for_user(current_user).find_by(id: data["postId"])
          conflict = original_text != post.raw

          if post.post_number == 1
            conflict ||= original_title.present? && original_title != post.topic.title

            # Since the topic might have hidden tags the current editor can't see,
            # we need to check for conflicts even though there might not be any visible tags in the editor
            if !conflict
              original_tags ||= []
              original_tag_ids = original_tags.filter_map { |t| t["id"] if t.is_a?(Hash) }
              # old draft format is tag names as strings
              old_format_names = original_tags.select { |t| t.is_a?(String) }
              original_tag_ids +=
                Tag.where(name: old_format_names).pluck(:id) if old_format_names.present?

              current_tag_ids = post.topic.tags.pluck(:id).to_set
              hidden_tag_ids = DiscourseTagging.hidden_tags(@guardian).pluck(:id).to_set
              conflict = original_tag_ids.to_set != (current_tag_ids - hidden_tag_ids)
            end
          end

          if conflict
            conflict_user = BasicUserSerializer.new(post.last_editor, root: false)
            json.merge!(conflict_user:)
          end
        end
      end
    end

    render json: json
  end

  def destroy
    user =
      if is_api?
        if @guardian.is_admin?
          fetch_user_from_params
        else
          raise Discourse::InvalidAccess
        end
      else
        current_user
      end

    begin
      Draft.clear(user, params[:id], params[:sequence].to_i)
    rescue Draft::OutOfSequence
      # nothing really we can do here, if try clearing a draft that is not ours, just skip it.
      # rendering an error causes issues in the composer
    rescue StandardError
      return render json: failed_json, status: :unauthorized
    end

    render json: success_json
  end

  def bulk_destroy
    params.require(:draft_keys)

    draft_keys = params[:draft_keys]

    if draft_keys.length > BULK_DESTROY_LIMIT
      raise Discourse::InvalidParameters.new(
              I18n.t("draft.bulk_destroy_limit", limit: BULK_DESTROY_LIMIT),
            )
    end

    sequences = params[:sequences] || {}

    return render json: success_json.merge(deleted_count: 0) if draft_keys.empty?

    user =
      if is_api?
        if @guardian.is_admin?
          fetch_user_from_params
        else
          raise Discourse::InvalidAccess
        end
      else
        current_user
      end

    # Validate all sequences first (fail fast)
    sequence_errors = []
    draft_keys.each do |draft_key|
      begin
        current_sequence = DraftSequence.current(user, draft_key)
        provided_sequence = sequences[draft_key].to_i
        sequence_errors << draft_key if provided_sequence != current_sequence
      rescue StandardError
        # If we can't get sequence for some reason, skip validation for this draft
        # This maintains the same lenient behavior as the single delete
      end
    end

    if sequence_errors.any?
      render json:
               failed_json.merge(
                 errors: "Draft sequence conflict for keys: #{sequence_errors.join(", ")}",
               ),
             status: :conflict
      return
    end

    # Bulk delete in single transaction
    deleted_count = 0
    begin
      ActiveRecord::Base.transaction do
        deleted_count = Draft.where(user_id: user.id, draft_key: draft_keys).destroy_all.length

        # Update user draft count
        UserStat.update_draft_count(user.id)
      end
    rescue StandardError
      return render json: failed_json, status: :internal_server_error
    end

    render json: success_json.merge(deleted_count: deleted_count)
  end

  private

  def reached_max_drafts_per_user?(params)
    user_id = current_user.id

    Draft.where(user_id: user_id).count >= SiteSetting.max_drafts_per_user &&
      !Draft.exists?(user_id: user_id, draft_key: params[:draft_key])
  end
end
