require 'digest'

class MaterialSyncImporter
  Result = Struct.new(:material, :action, :suppressed, :suppression, keyword_init: true)

  def self.import! attributes
    new(attributes).import!
  end

  def initialize attributes
    @attributes = attributes
  end

  def import!
    source = source_attributes
    key = normalized_source_key(source)
    raise ArgumentError, 'normalized_source_key is required for material sync' if key.blank?

    suppression = suppression_for(source)
    if suppression
      return Result.new(material: nil,
                        action: :suppressed,
                        suppressed: true,
                        suppression:)
    end

    material =
        Material
        .unscoped
        .find_or_initialize_by(normalized_source_key: key)
    if unchanged?(material, source)
      return Result.new(material:,
                        action: :unchanged,
                        suppressed: false,
                        suppression: nil)
    end

    event_type =
        if material.new_record?
          :create
        elsif material.discarded?
          :restore
        else
          :update
        end
    uploaded_blob = uploaded_blob!
    if @import_block
      return Result.new(material: nil,
                        action: :suppressed,
                        suppressed: true,
                        suppression: @import_block)
    end

    Material.transaction do
      if material.persisted?
        MaterialVersionRecorder.ensure_snapshot!(material,
                                                 created_by_user: @attributes[:updated_by_user])
      end
      material.assign_attributes(material_attributes_for(material).merge(source))
      material.discarded_at = nil if material.respond_to?(:discarded_at=)
      material.file.attach(uploaded_blob) if uploaded_blob
      material.save!
      upsert_export_item!(material)
      MaterialVersionRecorder.record!(material:,
                                      event_type:,
                                      created_by_user: @attributes[:updated_by_user])
    end
    MaterialThumbnailGenerator.generate!(material)

    Result.new(material:,
               action: event_type == :create ? :imported : :updated,
               suppressed: false,
               suppression: nil)
  rescue StandardError
    uploaded_blob&.purge_later
    raise
  ensure
    close_tempfile!
  end

  private

  def source_attributes
    { source_kind: @attributes[:source_kind],
      source_uri: @attributes[:source_uri],
      source_path: @attributes[:source_path],
      source_file_id: @attributes[:source_file_id] }
  end

  def normalized_source_key source
    MaterialSyncSuppression.normalize_source_key(source_kind: source[:source_kind],
                                                 source_uri: source[:source_uri],
                                                 source_path: source[:source_path],
                                                 source_file_id: source[:source_file_id])
  end

  def material_attributes_for material
    attrs =
        @attributes
        .except(:source_kind, :source_uri, :source_path, :source_file_id,
                :file_blob, :file_tempfile, :file_downloader,
                :filename, :content_type,
                :file_sha256, :export_path, :profile, :tag, :url)

    if material.new_record?
      attrs[:tag] = @attributes[:tag] if @attributes.key?(:tag)
      attrs[:url] = @attributes[:url] if @attributes.key?(:url)
      return attrs
    end

    if @attributes.key?(:tag) && @attributes[:tag].present?
      attrs[:tag] = @attributes[:tag]
    end
    if @attributes.key?(:url) && @attributes[:url].present?
      attrs[:url] = @attributes[:url]
    end

    attrs
  end

  def suppression_for source
    if source[:source_kind] == 'google_drive_file' && source[:source_path].present?
      return MaterialSyncSuppressionMatcher.match_google_drive_candidate(
          drive_file_id: source[:source_file_id],
          relative_path: source[:source_path])
    end

    MaterialSyncSuppressionMatcher.match(**source)
  end

  def uploaded_blob!
    return @attributes[:file_blob] if @attributes[:file_blob]

    tempfile = upload_tempfile
    return nil unless tempfile

    tempfile.rewind
    file_sha256 = @attributes[:file_sha256] || Digest::SHA256.file(tempfile.path).hexdigest
    @attributes[:file_sha256] = file_sha256

    @import_block = MaterialImportBlockMatcher.match_for_sha256(file_sha256)
    if @import_block
      tempfile.rewind
      return nil
    end

    blob = ActiveStorage::Blob.create_and_upload!(
        io: tempfile,
        filename: @attributes[:filename],
        content_type: @attributes[:content_type])
    MaterialFileSha256.assign_metadata_sha256!(blob, file_sha256)
    tempfile.rewind
    blob
  end

  def upload_tempfile
    return @upload_tempfile if defined?(@upload_tempfile)

    @upload_tempfile = @attributes[:file_tempfile]
    if @upload_tempfile.blank? && @attributes[:file_downloader]
      @upload_tempfile = @attributes[:file_downloader].call
    end
    @upload_tempfile
  end

  def upsert_export_item! material
    export_path = resolved_export_path(material)
    return if export_path.blank?

    item = material.material_export_items.find_or_initialize_by(profile: effective_profile)
    item.export_path = export_path
    item.enabled = true
    item.created_by_user ||= @attributes[:created_by_user]
    item.save!
  end

  def export_path_taken_by_other? material, profile, export_path
    MaterialExportItem
      .where(profile:, export_path:)
      .where.not(material_id: material.id)
      .exists?
  end

  def close_tempfile!
    tempfile = upload_tempfile
    return unless tempfile

    tempfile.close! unless tempfile.closed?
  rescue StandardError
    nil
  end

  def unchanged? material, source
    return false if material.new_record? || material.discarded?
    return false unless same_source_attributes?(material, source)
    return false unless same_export_path?(material)
    return false unless same_material_attributes?(material)

    same_file_snapshot?(material)
  end

  def same_source_attributes? material, source
    material.source_kind == source[:source_kind] \
      && material.source_uri == source[:source_uri] \
      && material.source_path == source[:source_path] \
      && material.source_file_id == source[:source_file_id]
  end

  def same_export_path? material
    expected = resolved_export_path(material)
    current = material.material_export_items.find { |item| item.profile == effective_profile }

    return current.blank? if expected.blank?

    current&.enabled && current.export_path == expected
  end

  def same_material_attributes? material
    same_tag_attribute?(material) && same_url_attribute?(material)
  end

  def same_file_snapshot? material
    expected_sha256 = @attributes[:file_sha256].to_s.presence

    if expected_sha256.present?
      return false unless material.file.attached?

      return MaterialFileSha256.metadata_sha256(material.file.blob) == expected_sha256
    end

    !material.file.attached?
  end

  def effective_profile
    @attributes[:profile].presence || 'legacy_drive'
  end

  def resolved_export_path material
    export_path = @attributes[:export_path].to_s.strip
    return export_path if export_path.blank?
    return export_path unless export_path_taken_by_other?(material, effective_profile, export_path)

    MaterialSyncExportPath.uniquify(export_path, @attributes[:source_file_id])
  end

  def same_tag_attribute? material
    return true unless @attributes.key?(:tag)
    return true if @attributes[:tag].blank?

    material.tag_id == @attributes[:tag].id
  end

  def same_url_attribute? material
    return true unless @attributes.key?(:url)
    return true if @attributes[:url].blank?

    material.url == @attributes[:url]
  end
end
