# typed: strict
class BlobEntriesController < ApplicationController
  skip_before_action :authenticate_user!, only: [:show]

  sig { void }
  def show
    thumb = params[:thumb]
    if thumb.present? && !thumb_params(thumb)
      raise ActionController::BadRequest.new("invalid thumbnail #{thumb}")
    end

    if thumb.present?
      expires_dur = 1.week
    else
      expires_dur = 1.year
    end
    response.headers["Expires"] = expires_dur.from_now.httpdate
    expires_in expires_dur, public: true

    unless stale?(
             last_modified: Time.at(0),
             strong_etag: strong_etag_for_request,
           )
      return
    end

    sha256 = T.let(params[:sha256], String)
    raise ActionController::BadRequest.new("no file specified") if sha256.blank?

    if show_blob_file(sha256, thumb)
      return
    else
      raise ActiveRecord::RecordNotFound
    end
  end

  private

  sig { params(sha256: String, thumb: T.nilable(String)).returns(T::Boolean) }
  def show_blob_file(sha256, thumb)
    if thumb
      thumb_params = thumb_params(thumb)
      if thumb_params.nil?
        raise ActionController::BadRequest.new("invalid thumbnail: #{thumb}")
      end

      # if the requested format is gif, and the thumbnail type is content-container, we want to
      # thumbnail the gif into another gif. Else, always thumbnail into a jpeg.
      file_ext = "jpeg"
      if params[:format] == "gif" && thumb == "content-container"
        file_ext = "gif"
      end

      width, height = thumb_params
      filename = "thumb-#{sha256}-#{thumb}.#{file_ext}"
      cache_key = "vips:#{filename}"
      thumb_data =
        Rack::MiniProfiler.step("vips: load from cache") do
          Rails
            .cache
            .fetch(cache_key, expires_in: 1.day) do
              blob_file = BlobFile.find_by(sha256: HexUtil.hex2bin(sha256))
              if blob_file
                content_type =
                  blob_file.content_type || "application/octet-stream"
                if helpers.is_renderable_video_type?(content_type)
                  thumbnail_video_file(blob_file, width, height, file_ext)
                elsif helpers.is_renderable_image_type?(content_type)
                  thumbnail_image_file(blob_file, width, height, file_ext)
                end
              end
            end
        end

      if !thumb_data
        Rails.cache.delete(cache_key)
        return false
      end

      send_data(
        thumb_data[0],
        type: thumb_data[1],
        disposition: "inline",
        filename: filename,
      )
    else
      blob_file = BlobFile.find_by(sha256: HexUtil.hex2bin(sha256))
      return false if !blob_file

      content_type = blob_file.content_type || "application/octet-stream"
      send_file(
        blob_file.absolute_file_path,
        type: content_type,
        disposition: "inline",
      )
    end

    return true
  end

  sig do
    params(
      blob_file: BlobFile,
      width: Integer,
      height: Integer,
      thumb: String,
    ).returns(T.nilable([String, String]))
  end
  def thumbnail_video_file(blob_file, width, height, thumb)
    video_file = blob_file.absolute_file_path
    temp_thumb_file = Tempfile.new(%w[video-thumb .png])
    process_result =
      system(
        "ffmpegthumbnailer",
        "-f", # overlay video strip indicator
        "-i",
        video_file,
        "-o",
        T.must(temp_thumb_file.path),
        "-s",
        "#{width}",
        "-c",
        "jpeg",
      )
    if !process_result
      temp_thumb_file.unlink
      return nil
    end

    thumb_data_tmp = File.read(T.must(temp_thumb_file.path), mode: "rb")
    temp_thumb_file.unlink
    [thumb_data_tmp, "image/jpeg"]
  end

  # Returns a tuple of the thumbnail data and the content type
  sig do
    params(
      blob_file: BlobFile,
      width: Integer,
      height: Integer,
      file_ext: String,
    ).returns(T.nilable([String, String]))
  end
  def thumbnail_image_file(blob_file, width, height, file_ext)
    blob_file_path = blob_file.absolute_file_path

    if file_ext == "gif"
      VipsUtil.try_load_gif(
        blob_file_path,
        load_gif: -> do
          Rack::MiniProfiler.step("vips: load gif") do
            # Use libvips' gifload with n=-1 to load all frames
            image = Vips::Image.gifload(blob_file_path, n: -1)
            num_frames = image.get("n-pages")
            image_width, image_height = image.width, (image.height / num_frames)

            if width >= image_width && height >= image_height
              logger.info(
                "gif is already smaller than requested thumbnail size",
              )
              return File.binread(blob_file_path), "image/gif"
            end

            Rack::MiniProfiler.step("vips: thumbnail gif") do
              image = image.thumbnail_image(width, height: height)
              image_buffer =
                image.gifsave_buffer(
                  dither: 1,
                  effort: 1,
                  interframe_maxerror: 16,
                  interpalette_maxerror: 10,
                  interlace: true,
                )
              [image_buffer, "image/gif"]
            end
          end
        end,
        on_load_failed: ->(detected_content_type) do
          case detected_content_type
          when %r{image/png}
            thumbnail_image_file(blob_file, width, height, "png")
          when %r{image/jpeg}, %r{image/jpg}
            thumbnail_image_file(blob_file, width, height, "jpeg")
          else
            raise
          end
        end,
      )
    else
      # Original static image thumbnailing logic
      image_buffer =
        Rack::MiniProfiler.step("vips: load image") do
          T.unsafe(Vips::Image).thumbnail(
            blob_file.absolute_file_path,
            width,
            height: height,
          )
        end

      Rack::MiniProfiler.step("vips: thumbnail image") do
        logger.info("rendering thumbnail as jpeg")
        [
          T.let(image_buffer.jpegsave_buffer(interlace: true, Q: 95), String),
          "image/jpeg",
        ]
      end
    end
  end

  sig { params(thumb: String).returns(T.nilable([Integer, Integer])) }
  def thumb_params(thumb)
    case thumb
    when "32-avatar"
      [32, 32]
    when "64-avatar"
      [64, 64]
    when "tiny"
      [100, 100]
    when "small"
      [400, 300]
    when "medium"
      [800, 600]
    when "content-container"
      [768, 2048]
    end
  end

  sig { returns(String) }
  def strong_etag_for_request
    [params[:sha256], params[:thumb], params[:format]].compact.join("-")
  end
end
