# frozen_string_literal: true

require "csv"

class Admin::BadgesController < Admin::AdminController
  MAX_CSV_LINES = 50_000
  BATCH_SIZE = 200

  def index
    data = {
      badge_types: BadgeType.all.order(:id).to_a,
      badge_groupings: BadgeGrouping.all.order(:position).to_a,
      badges:
        Badge
          .includes(:badge_grouping)
          .includes(:badge_type, :image_upload)
          .references(:badge_grouping)
          .order("enabled DESC", "badge_groupings.position, badge_type_id, badges.name")
          .to_a,
      protected_system_fields: Badge.protected_system_fields,
      triggers: Badge.trigger_hash,
    }
    render_serialized(OpenStruct.new(data), AdminBadgesSerializer)
  end

  def preview
    return render json: "preview not allowed", status: 403 unless SiteSetting.enable_badge_sql

    render json:
             BadgeGranter.preview(
               params[:sql],
               target_posts: params[:target_posts] == "true",
               explain: params[:explain] == "true",
               trigger: params[:trigger].to_i,
             )
  end

  def new
  end

  def show
  end

  def award
  end

  def mass_award
    csv_file = params.permit(:file).fetch(:file, nil)
    badge = Badge.find_by(id: params[:badge_id])
    raise Discourse::InvalidParameters if csv_file.try(:tempfile).nil? || badge.nil?

    if !badge.enabled?
      render_json_error(
        I18n.t("badges.mass_award.errors.badge_disabled", badge_name: badge.display_name),
        status: 422,
      )
      return
    end

    replace_badge_owners = params[:replace_badge_owners] == "true"
    ensure_users_have_badge_once = params[:grant_existing_holders] != "true"
    if !ensure_users_have_badge_once && !badge.multiple_grant?
      render_json_error(
        I18n.t(
          "badges.mass_award.errors.cant_grant_multiple_times",
          badge_name: badge.display_name,
        ),
        status: 422,
      )
      return
    end

    line_number = 1
    usernames = []
    emails = []
    File.open(csv_file) do |csv|
      csv.each_line do |line|
        line = CSV.parse_line(line).first&.strip
        line_number += 1

        if line.present?
          if line.include?("@")
            emails << line
          else
            usernames << line
          end
        end

        if emails.size + usernames.size > MAX_CSV_LINES
          return(
            render_json_error I18n.t(
                                "badges.mass_award.errors.too_many_csv_entries",
                                count: MAX_CSV_LINES,
                              ),
                              status: 400
          )
        end
      end
    end
    BadgeGranter.revoke_all(badge) if replace_badge_owners

    results =
      BadgeGranter.enqueue_mass_grant_for_users(
        badge,
        emails: emails,
        usernames: usernames,
        ensure_users_have_badge_once: ensure_users_have_badge_once,
      )

    render json: {
             unmatched_entries: results[:unmatched_entries].first(100),
             matched_users_count: results[:matched_users_count],
             unmatched_entries_count: results[:unmatched_entries_count],
           },
           status: :ok
  rescue CSV::MalformedCSVError
    render_json_error I18n.t("badges.mass_award.errors.invalid_csv", line_number: line_number),
                      status: 400
  end

  def badge_types
    badge_types = BadgeType.all.to_a
    render_serialized(badge_types, BadgeTypeSerializer, root: "badge_types")
  end

  def save_badge_groupings
    badge_groupings = BadgeGrouping.all.order(:position).to_a
    ids = params[:ids].map(&:to_i)

    params[:names].each_with_index do |name, index|
      id = ids[index].to_i
      group = badge_groupings.find { |b| b.id == id } || BadgeGrouping.new
      group.name = name
      group.position = index
      group.save
    end

    badge_groupings.each { |g| g.destroy unless g.system? || ids.include?(g.id) }

    badge_groupings = BadgeGrouping.all.order(:position).to_a
    render_serialized(badge_groupings, BadgeGroupingSerializer, root: "badge_groupings")
  end

  def create
    badge = Badge.new
    errors = update_badge_from_params(badge, new: true)

    if errors.present?
      render_json_error errors
    else
      StaffActionLogger.new(current_user).log_badge_creation(badge)
      render_serialized(badge, AdminBadgeSerializer, root: "badge")
    end
  end

  def update
    badge = find_badge
    errors = update_badge_from_params(badge)

    if errors.present?
      render_json_error errors
    else
      StaffActionLogger.new(current_user).log_badge_change(badge)
      render_serialized(badge, AdminBadgeSerializer, root: "badge")
    end
  end

  def destroy
    Badge.transaction do
      badge = find_badge
      StaffActionLogger.new(current_user).log_badge_deletion(badge)
      badge.clear_user_titles!
      badge.destroy!
    end
    render body: nil
  end

  private

  def find_badge
    params.require(:id)
    Badge.find(params[:id])
  end

  # Options:
  #   :new - reset the badge id to nil before saving
  def update_badge_from_params(badge, opts = {})
    errors = []
    Badge.transaction do
      allowed = Badge.column_names.map(&:to_sym)
      allowed -= %i[id created_at updated_at grant_count]
      allowed -= Badge.protected_system_fields if badge.system?
      allowed -= [:query] unless SiteSetting.enable_badge_sql

      params.permit(*allowed)

      allowed.each { |key| badge.public_send("#{key}=", params[key]) if params[key] }

      # Badge query contract checks
      begin
        if SiteSetting.enable_badge_sql
          BadgeGranter.contract_checks!(
            badge.query,
            target_posts: badge.target_posts,
            trigger: badge.trigger,
          )
        end
      rescue => e
        errors << e.message
        raise ActiveRecord::Rollback
      end

      badge.id = nil if opts[:new]
      badge.save!
    end

    if opts[:new].blank?
      Jobs.enqueue(
        :bulk_user_title_update,
        new_title: badge.name,
        granted_badge_id: badge.id,
        action: Jobs::BulkUserTitleUpdate::UPDATE_ACTION,
      )
    end

    errors
  rescue ActiveRecord::RecordInvalid
    errors.push(*badge.errors.full_messages)
    errors
  end
end