# frozen_string_literal: true
class Course::Material < ApplicationRecord
  has_one_attachment
  include DuplicationStateTrackingConcern
  include Workflow

  workflow do
    state :not_chunked do
      event :start_chunking, transitions_to: :chunking
    end
    # State where there is a job running to chunk course materials
    state :chunking do
      event :finish_chunking, transitions_to: :chunked
      event :cancel_chunking, transitions_to: :not_chunked
    end
    # The state where chunking job is completed and course_materials is chunked
    state :chunked do
      event :delete_chunks, transitions_to: :not_chunked
    end
  end

  belongs_to :folder, inverse_of: :materials, class_name: 'Course::Material::Folder'
  has_many :text_chunk_references, inverse_of: :material, class_name: 'Course::Material::TextChunkReference',
                                   dependent: :destroy, autosave: true
  has_many :text_chunks, through: :text_chunk_references
  has_one :text_chunking, class_name: 'Course::Material::TextChunking',
                          dependent: :destroy, inverse_of: :material, autosave: true

  before_save :touch_folder

  validate :validate_name_is_unique_among_folders
  validates_with FilenameValidator
  validates :name, length: { maximum: 255 }, presence: true
  validates :creator, presence: true
  validates :updater, presence: true
  validates :folder, presence: true
  validates :name, uniqueness: { scope: [:folder_id], case_sensitive: false,
                                 if: -> { folder_id? && name_changed? } }
  validates :folder_id, uniqueness: { scope: [:name], case_sensitive: false,
                                      if: -> { name? && folder_id_changed? } }
  validates :workflow_state, presence: true

  scope :in_concrete_folder, -> { joins(:folder).merge(Folder.concrete) }

  class << self
    def text_chunking!(material_ids, current_user)
      materials = Course::Material.where(id: material_ids)
      return if materials.empty?

      materials.each(&:ensure_text_chunking!)
      Course::Material::TextChunkJob.perform_later(material_ids, current_user).tap do |job|
        materials.each do |material|
          material.text_chunking.update_column(:job_id, job.job_id)
        end
      end
    end

    def destroy_text_chunk_references(material_ids)
      ActiveRecord::Base.transaction do
        materials = Course::Material.includes(:text_chunk_references).where(id: material_ids, workflow_state: 'chunked')
        materials.each do |material|
          material.text_chunk_references.destroy_all
          material.delete_chunks!
          material.save!
        end
      end
      true
    end
  end

  def touch_folder
    folder.touch if !duplicating? && changed?
  end

  # Returns the path of the material
  #
  # @return [Pathname] The path of the material
  def path
    folder.path + name
  end

  # Return false to prevent the userstamp gem from changing the updater during duplication
  def record_userstamp
    !duplicating?
  end

  # Finds a unique name for the current material among its siblings.
  #
  # @return [String] A unique name.
  def next_valid_name
    folder.next_uniq_child_name(self)
  end

  def initialize_duplicate(duplicator, other)
    self.attachment = duplicator.duplicate(other.attachment)
    self.text_chunk_references = other.text_chunk_references.
                                 map { |text_chunk_reference| duplicator.duplicate(text_chunk_reference) }
    self.folder = if duplicator.duplicated?(other.folder)
                    duplicator.duplicate(other.folder)
                  else
                    # If parent has not been duplicated yet, put the current duplicate under the root folder
                    # temorarily. The material will be re-parented only afterwards when the parent folder is being
                    # duplicated. This will be done when `#initialize_duplicate_children` is called on the
                    # duplicated parent folder.
                    #
                    # If the material's folder is not selected for duplication, the current duplicated material will
                    # remain a child of the root folder.
                    duplicator.options[:destination_course].root_folder
                  end
    self.updated_at = other.updated_at
    self.created_at = other.created_at
    set_duplication_flag
  end

  def before_duplicate_save(_duplicator)
    self.name = next_valid_name
  end

  def build_text_chunks(current_user)
    file_name = attachment.name
    attachment.open(encoding: 'ASCII-8BIT') do |file|
      existing_text_chunks = Course::Material::TextChunk.existing_chunks(file: file)
      if existing_text_chunks.exists?
        create_references_for_existing_chunks(existing_text_chunks, current_user)
      else
        create_new_chunks_and_references(current_user, file, file_name)
      end
    end
    save!
  end

  def ensure_text_chunking!
    ActiveRecord::Base.transaction(requires_new: true) do
      text_chunking || create_text_chunking!
    end
  rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e
    raise e if e.is_a?(ActiveRecord::RecordInvalid) && e.record.errors[:material_id].empty?

    association(:text_chunking).reload
    text_chunking
  end

  private

  # TODO: Not threadsafe, consider making all folders as materials
  # Make sure that material won't have the same name with other child folders in the folder
  # Schema validations already ensure that it won't have the same name as other materials
  def validate_name_is_unique_among_folders
    return if folder.nil?

    conflicts = folder.children.where('name ILIKE ?', name)
    errors.add(:name, :taken) unless conflicts.empty?
  end

  def create_references_for_existing_chunks(existing_chunks, current_user)
    existing_chunks.find_each do |chunk|
      text_chunk_references.build(
        text_chunk: chunk,
        creator: current_user,
        updater: current_user
      )
    end
  end

  def create_new_chunks_and_references(current_user, file, file_name)
    llm_service = RagWise::LlmService.new
    chunking_service = RagWise::ChunkingService.new(file: file, file_name: file_name)

    file_digest = Digest::SHA256.file(file.try(:tempfile) || file).hexdigest
    chunks = chunking_service.file_chunking
    embeddings = llm_service.generate_embeddings_from_chunks(chunks)
    chunks.each_with_index do |chunk, index|
      text_chunk_references.build(
        text_chunk: Course::Material::TextChunk.new(
          name: file_digest,
          embedding: embeddings[index],
          content: chunk
        ),
        creator: current_user,
        updater: current_user
      )
    end
  end
end
