# frozen_string_literal: true

module Decidim
  # this class represents a content block manifest instance in the DB. Check the
  # docs on `ContentBlockRegistry` and `ContentBlockManifest` for more info.
  class ContentBlock < ApplicationRecord
    include Publicable

    attr_accessor :in_preview

    belongs_to :organization, foreign_key: :decidim_organization_id, class_name: "Decidim::Organization"
    has_many :attachments, foreign_key: "decidim_content_block_id", class_name: "Decidim::ContentBlockAttachment", inverse_of: :content_block, dependent: :destroy

    delegate :public_name_key, :has_settings?, :settings_form_cell, :cell, :component_manifest_name, to: :manifest

    before_save :save_images
    after_save :reload_images

    validates_associated :images_container

    # Public: finds the published content blocks for the given scope and
    # organization. Returns them ordered by ascending weight (lowest first).
    def self.for_scope(scope, organization:)
      where(organization:, scope_name: scope)
        .order(weight: :asc)
    end

    def manifest
      @manifest ||= Decidim.content_blocks.for(scope_name).find { |manifest| manifest.name.to_s == manifest_name }
    end

    # Public: Uses the `SettingsManifest` class to generate a settings schema
    # and fill it with the content blocks current settings. This eases the
    # access to those settings values.
    #
    # Returns an object that responds to the settings defined in the content
    # block manifest.
    def settings
      manifest.settings.schema.new(self[:settings])
    end

    def reload(*)
      @manifest_attachments = nil
      @images_container = nil
      super
    end

    # Public: Holds access to the images related to the content block. This
    # method generates a dynamic class that encapsulates the uploaders for the
    # content block images, and eases the access to them. It is a little bit
    # hacky, but it is the only way I could come up with in order to let content
    # block images have different uploaders.
    #
    # Examples:
    #
    #     # This will process the image with the uploader defined at the
    #     # manifest, upload it and save the record.
    #     content_block.images_container.my_image = params[:my_image]
    #     content_block.save!
    #
    #     # This is how you can access the image data, just like with any other
    #     # uploader field. You can use the uploader variants too.
    #     content_block.images_container.attached_uploader(:my_image).url
    #     content_block.images_container.attached_uploader(:my_image).variant_url(:big)
    #
    #     # This will delete the attached image
    #     content_block.images_container.my_image = nil
    #     content_block.save!
    #
    # Returns an object that responds to the image names defined in the content
    # block manifest.
    def images_container
      return @images_container if @images_container

      attachments = manifest_attachments

      @images_container = Class.new do
        include ActiveModel::Validations
        include Decidim::HasUploadValidations

        cattr_accessor :manifest_attachments
        attr_reader :content_block

        def self.name
          to_s.camelize
        end

        delegate :id, :organization, to: :content_block

        def initialize(content_block)
          @content_block = content_block
        end

        attachments.each do |name, attachment|
          validates_upload name, uploader: attachment.uploader

          define_method(name) do
            attachment.file
          end

          define_method("#{name}=") do |file|
            attachment.file = file
          end
        end

        def attached_uploader(name)
          return if manifest_attachments[name].blank?

          manifest_attachments[name].attached_uploader(:file)
        end

        def save
          return unless content_block.persisted?

          manifest_attachments.values.map(&:save).all?
        end
      end

      @images_container.manifest_attachments = manifest_attachments
      @images_container = @images_container.new(self)
    end

    private

    def manifest_attachments
      @manifest_attachments ||= manifest.images.each_with_object({}) do |attachment_config, list|
        list[attachment_config[:name]] = attachments.find_or_initialize_by(name: attachment_config[:name])
      end
    end

    # Internal: Since we are using the `images_container` hack to hold the
    # uploaders, we need to manually trigger it to save the attached images.
    def save_images
      images_container.save
    end

    # On instance reloading we need to remove the `images_container` cached
    # class so it gets regenerated with the new values.
    def reload_images
      @images_container = nil
    end
  end
end
