# frozen_string_literal: true

class UserAvatar < ActiveRecord::Base
  belongs_to :user
  belongs_to :gravatar_upload, class_name: "Upload"
  belongs_to :custom_upload, class_name: "Upload"
  has_many :upload_references, as: :target, dependent: :destroy

  after_save do
    if saved_change_to_custom_upload_id? || saved_change_to_gravatar_upload_id?
      upload_ids = [self.custom_upload_id, self.gravatar_upload_id]
      UploadReference.ensure_exist!(upload_ids: upload_ids, target: self)
    end
  end

  @@custom_user_gravatar_email_hash = {
    Discourse::SYSTEM_USER_ID => User.email_hash("info@discourse.org"),
  }

  def self.register_custom_user_gravatar_email_hash(user_id, email)
    @@custom_user_gravatar_email_hash[user_id] = User.email_hash(email)
  end

  def contains_upload?(id)
    gravatar_upload_id == id || custom_upload_id == id
  end

  def update_gravatar!
    DistributedMutex.synchronize("update_gravatar_#{user_id}") do
      begin
        self.update!(last_gravatar_download_attempt: Time.zone.now)

        max = Discourse.avatar_sizes.max

        # The user could be deleted before this executes
        return if user.blank? || user.primary_email.blank?

        email_hash = @@custom_user_gravatar_email_hash[user_id] || user.email_hash
        gravatar_url =
          "https://#{SiteSetting.gravatar_base_url}/avatar/#{email_hash}.png?s=#{max}&d=404&reset_cache=#{SecureRandom.urlsafe_base64(5)}"

        if SiteSetting.verbose_upload_logging
          Rails.logger.warn("Verbose Upload Logging: Downloading gravatar from #{gravatar_url}")
        end

        # follow redirects in case gravatar change rules on us
        tempfile =
          FileHelper.download(
            gravatar_url,
            max_file_size: SiteSetting.max_image_size_kb.kilobytes,
            tmp_file_name: "gravatar",
            skip_rate_limit: true,
            verbose: false,
            follow_redirect: true,
          )

        if tempfile
          ext = File.extname(tempfile)
          ext = ".png" if ext.blank?

          upload =
            UploadCreator.new(
              tempfile,
              "gravatar#{ext}",
              origin: gravatar_url,
              type: "avatar",
              for_gravatar: true,
            ).create_for(user_id)

          if gravatar_upload_id != upload.id
            User.transaction do
              if gravatar_upload_id && user.uploaded_avatar_id == gravatar_upload_id
                user.update!(uploaded_avatar_id: upload.id)
              end

              self.update!(gravatar_upload: upload)
            end
          end
        end
      rescue OpenURI::HTTPError => e
        raise e if e.io&.status&.[](0).to_i != 404
      ensure
        tempfile&.close!
      end
    end
  end

  def self.local_avatar_url(hostname, username, upload_id, size)
    self.local_avatar_template(hostname, username, upload_id).gsub("{size}", size.to_s)
  end

  def self.local_avatar_template(hostname, username, upload_id)
    version = self.version(upload_id)
    "#{Discourse.base_path}/user_avatar/#{hostname}/#{username}/{size}/#{version}.png"
  end

  def self.external_avatar_url(user_id, upload_id, size)
    self.external_avatar_template(user_id, upload_id).gsub("{size}", size.to_s)
  end

  def self.external_avatar_template(user_id, upload_id)
    version = self.version(upload_id)
    "#{Discourse.store.absolute_base_url}/avatars/#{user_id}/{size}/#{version}.png"
  end

  def self.version(upload_id)
    "#{upload_id}_#{OptimizedImage::VERSION}"
  end

  def self.import_url_for_user(avatar_url, user, options = nil)
    if SiteSetting.verbose_upload_logging
      Rails.logger.warn("Verbose Upload Logging: Downloading sso-avatar from #{avatar_url}")
    end

    tempfile =
      FileHelper.download(
        avatar_url,
        max_file_size: SiteSetting.max_image_size_kb.kilobytes,
        tmp_file_name: "sso-avatar",
        follow_redirect: true,
        skip_rate_limit: !!options&.fetch(:skip_rate_limit, false),
      )

    return unless tempfile

    ext = FastImage.type(tempfile).to_s
    tempfile.rewind

    upload =
      UploadCreator.new(
        tempfile,
        "external-avatar." + ext,
        origin: avatar_url,
        type: "avatar",
      ).create_for(user.id)

    user.create_user_avatar! unless user.user_avatar

    if !user.user_avatar.contains_upload?(upload.id)
      user.user_avatar.update!(custom_upload_id: upload.id)
      override_gravatar = !options || options[:override_gravatar]

      if user.uploaded_avatar_id.nil? ||
           !user.user_avatar.contains_upload?(user.uploaded_avatar_id) || override_gravatar
        user.update!(uploaded_avatar_id: upload.id)
      end
    end
  rescue Net::ReadTimeout, OpenURI::HTTPError, FinalDestination::SSRFError
    # Skip saving. We are not connected to the net, or SSRF checks failed.
  ensure
    tempfile.close! if tempfile && tempfile.respond_to?(:close!)
  end

  def self.ensure_consistency!(max_optimized_avatars_to_remove: 20_000)
    DB.exec <<~SQL
      UPDATE user_avatars
      SET gravatar_upload_id = NULL
      WHERE gravatar_upload_id IN (
        SELECT u1.gravatar_upload_id FROM user_avatars u1
        LEFT JOIN uploads up
          ON u1.gravatar_upload_id = up.id
        WHERE u1.gravatar_upload_id IS NOT NULL AND
          up.id IS NULL
      )
    SQL

    DB.exec <<~SQL
      UPDATE user_avatars
      SET custom_upload_id = NULL
      WHERE custom_upload_id IN (
        SELECT u1.custom_upload_id FROM user_avatars u1
        LEFT JOIN uploads up
          ON u1.custom_upload_id = up.id
        WHERE u1.custom_upload_id IS NOT NULL AND
          up.id IS NULL
      )
    SQL

    ids =
      DB.query_single(<<~SQL, sizes: Discourse.avatar_sizes, limit: max_optimized_avatars_to_remove)
      SELECT oi.id FROM (
        SELECT custom_upload_id FROM user_avatars
        EXCEPT
        SELECT upload_id FROM  upload_references WHERE target_type <> 'UserAvatar'
        AND upload_id IS NOT NULL
      ) AS a
      JOIN optimized_images oi ON oi.upload_id = a.custom_upload_id
      WHERE oi.width not in (:sizes) AND oi.height not in (:sizes)
      LIMIT :limit
    SQL

    warnings_reported = 0

    ids.each do |id|
      begin
        OptimizedImage.find(id).destroy!
      rescue ActiveRecord::RecordNotFound
      rescue => e
        if warnings_reported < 10
          Discourse.warn_exception(e, message: "Failed to remove optimized image")
          warnings_reported += 1
        end
      end
    end
  end
end

# == Schema Information
#
# Table name: user_avatars
#
#  id                             :integer          not null, primary key
#  user_id                        :integer          not null
#  custom_upload_id               :integer
#  gravatar_upload_id             :integer
#  last_gravatar_download_attempt :datetime
#  created_at                     :datetime         not null
#  updated_at                     :datetime         not null
#
# Indexes
#
#  index_user_avatars_on_custom_upload_id    (custom_upload_id)
#  index_user_avatars_on_gravatar_upload_id  (gravatar_upload_id)
#  index_user_avatars_on_user_id             (user_id)
#