# frozen_string_literal: true

require 'base64'
require 'pathname'
require 'uri'

module RubyLLM
  # A class representing a file attachment.
  class Attachment
    attr_reader :source, :filename, :mime_type

    DOCUMENT_EXTENSIONS = %w[
      doc docx dot key numbers odp ods odt pages pot pps ppt pptx rtf xls xlsx
    ].freeze
    ACTIVE_STORAGE_CLASS_NAMES = %w[
      ActiveStorage::Blob
      ActiveStorage::Attachment
      ActiveStorage::Attached::One
      ActiveStorage::Attached::Many
    ].freeze

    def initialize(source, filename: nil)
      @source = source
      @source = source_type_cast
      @filename = filename || source_filename

      determine_mime_type
    end

    def url?
      @source.is_a?(URI) || (@source.is_a?(String) && @source.match?(%r{^https?://}))
    end

    def path?
      @source.is_a?(Pathname) || (@source.is_a?(String) && !url?)
    end

    def io_like?
      @source.respond_to?(:read) && !path? && !active_storage?
    end

    def active_storage?
      ACTIVE_STORAGE_CLASS_NAMES.any? { |class_name| source_is_a?(class_name) }
    end

    def content
      return @content if defined?(@content) && !@content.nil?

      if url?
        fetch_content
      elsif path?
        load_content_from_path
      elsif active_storage?
        load_content_from_active_storage
      elsif io_like?
        load_content_from_io
      else
        RubyLLM.logger.warn "Source is neither a URL, path, ActiveStorage, nor IO-like: #{@source.class}"
        nil
      end

      @content
    end

    def encoded
      Base64.strict_encode64(content)
    end

    def save(path)
      return unless io_like?

      File.open(path, 'w') do |f|
        f.puts(@source.read)
      end
    end

    def for_llm
      case type
      when :text
        "<file name='#{filename}' mime_type='#{mime_type}'>#{content}</file>"
      else
        "data:#{mime_type};base64,#{encoded}"
      end
    end

    def type
      return :image if image?
      return :video if video?
      return :audio if audio?
      return :pdf if pdf?
      return :text if text?
      return :document if document?

      :unknown
    end

    def image?
      RubyLLM::MimeType.image? mime_type
    end

    def video?
      RubyLLM::MimeType.video? mime_type
    end

    def audio?
      RubyLLM::MimeType.audio? mime_type
    end

    def format
      case mime_type
      when 'audio/mpeg'
        'mp3'
      when 'audio/wav', 'audio/wave', 'audio/x-wav'
        'wav'
      else
        mime_type.split('/').last
      end
    end

    def pdf?
      RubyLLM::MimeType.pdf? mime_type
    end

    def document?
      return false if pdf? || text?

      RubyLLM::MimeType.document?(mime_type) || DOCUMENT_EXTENSIONS.include?(extension)
    end

    def extension
      extension = File.extname(filename.to_s).delete_prefix('.').downcase
      extension.empty? ? nil : extension
    end

    def text?
      RubyLLM::MimeType.text? mime_type
    end

    def to_h
      { type: type, source: @source }
    end

    private

    def determine_mime_type
      return @mime_type = active_storage_content_type if active_storage? && active_storage_content_type.present?

      @mime_type = RubyLLM::MimeType.for(url? ? nil : @source, name: @filename)
      @mime_type = RubyLLM::MimeType.for(content) if @mime_type == 'application/octet-stream'
      @mime_type = 'audio/wav' if @mime_type == 'audio/x-wav' # Normalize WAV type
    end

    def fetch_content
      response = Connection.basic.get @source.to_s
      @content = response.body
    end

    def load_content_from_path
      @content = File.binread(@source)
    end

    def load_content_from_io
      @source.rewind if @source.respond_to? :rewind
      @content = @source.read
    end

    def load_content_from_active_storage
      @content = active_storage_blob&.download
    end

    def source_type_cast
      if url?
        URI(@source)
      elsif path?
        Pathname.new(@source)
      else
        @source
      end
    end

    def source_filename
      if url?
        File.basename(@source.path).to_s
      elsif path?
        @source.basename.to_s
      elsif io_like?
        extract_filename_from_io
      elsif active_storage?
        extract_filename_from_active_storage
      end
    end

    def extract_filename_from_io
      if defined?(ActionDispatch::Http::UploadedFile) && @source.is_a?(ActionDispatch::Http::UploadedFile)
        @source.original_filename.to_s
      elsif @source.respond_to?(:path)
        File.basename(@source.path).to_s
      else
        'attachment'
      end
    end

    def extract_filename_from_active_storage
      active_storage_blob&.filename&.to_s || 'attachment'
    end

    def active_storage_content_type
      active_storage_blob&.content_type
    end

    def active_storage_blob
      return @source if source_is_a?('ActiveStorage::Blob')
      return @source.blob if source_is_a?('ActiveStorage::Attachment')
      return @source.blob if source_is_a?('ActiveStorage::Attached::One')

      @source.blobs.first if source_is_a?('ActiveStorage::Attached::Many')
    end

    def source_is_a?(class_name)
      klass = RubyLLM::Utils.safe_constantize(class_name)
      klass ? @source.is_a?(klass) : false
    end
  end
end
