# typed: strict
class BlobFile < ReduxApplicationRecord
  self.table_name = "blob_files"

  ROOT_DIR =
    T.let(
      if bfl = Rails.application.config_for("blob_file_location")
        bfl[0] == "/" ? bfl : Rails.root.join(bfl).to_s
      else
        raise("no blob_file_location config")
      end,
      String,
    )
  TMP_DIR = T.let(File.join(ROOT_DIR, "tmp-files"), String)

  FILE_PATH_PATTERNS =
    T.let({ v1: [2, 2, 1] }, T::Hash[Symbol, T::Array[Integer]])
  # consider sha256 `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
  # breaking it into 3 parts, becomes:
  # e3/b0/c/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
  # so with 100M files, each directory will have 100M / (256*256*16) = ~6 files

  include ImmutableModel
  before_destroy { raise ActiveRecord::ReadOnlyRecord }

  self.primary_key = :sha256
  EMPTY_FILE_SHA256 =
    T.let(
      HexUtil.hex2bin(
        "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
      ),
      String,
    )

  enum :version, { v1: 1 }
  after_initialize { self.version ||= :v1 }

  has_many :phash_thumbnails,
           class_name: "Phash::Thumbnail",
           foreign_key: :blob_sha256,
           primary_key: :sha256

  validates_presence_of(:sha256, :content_type, :size_bytes)
  validates :sha256, length: { is: 32 }
  validates :content_bytes,
            length: {
              minimum: 0,
              allow_nil: false,
              message: "can't be nil",
            }

  before_save :write_to_disk!
  sig { void }
  def write_to_disk!
    if not self.persisted?
      unless File.exist?(self.absolute_file_path)
        FileUtils.mkdir_p(File.dirname(self.absolute_file_path))
        FileUtils.mkdir_p(TMP_DIR)

        tmp_file_path = File.join(TMP_DIR, "blob-file-#{SecureRandom.uuid}")
        File.binwrite(tmp_file_path, T.must(self.content_bytes))
        FileUtils.mv(tmp_file_path, self.absolute_file_path)
      end
    end
  end

  sig do
    params(
      contents: String,
      block: T.proc.params(blob_file: BlobFile).void,
    ).returns(BlobFile)
  end
  def self.find_or_initialize_from_contents(contents, &block)
    BlobFile.find_or_initialize_by(
      sha256: Digest::SHA256.digest(contents),
    ) do |blob_file|
      blob_file.content_bytes = contents
      block.call(blob_file)
    end
  end

  sig { params(content_bytes: T.nilable(String)).void }
  def content_bytes=(content_bytes)
    raise("cannot set content_bytes of an existing BlobFile") if self.persisted?
    if content_bytes.nil?
      @content_bytes = nil
      self.size_bytes = 0
      self.sha256 = nil
    else
      content_bytes.force_encoding("ASCII-8BIT")
      @content_bytes = T.let(content_bytes, T.nilable(String))
      self.size_bytes = content_bytes.bytesize
      self.sha256 = Digest::SHA256.digest(content_bytes)
    end
  end

  sig { returns(T.nilable(String)) }
  def content_bytes
    if self.persisted?
      @content_bytes ||=
        begin
          c = File.binread(self.absolute_file_path)
          c.force_encoding("ASCII-8BIT")
          c
        end
    else
      @content_bytes
    end
  end

  sig { returns(String) }
  def absolute_file_path
    File.join(ROOT_DIR, self.relative_file_path)
  end

  sig { returns(T.nilable(String)) }
  def relative_file_path
    version = self.version
    return nil if version.nil?
    sha256 = self.sha256
    return nil if sha256.nil?
    pattern = FILE_PATH_PATTERNS[version.to_sym]
    return nil if pattern.nil?

    file_path_parts = [
      version,
      *self.class.path_segments(pattern, HexUtil.bin2hex(sha256)),
    ]
    file_path_parts.join("/")
  end

  sig do
    params(pattern: T::Array[Integer], sha256_hex: String).returns(
      T::Array[String],
    )
  end
  def self.path_segments(pattern, sha256_hex)
    parts = T.let([], T::Array[String])
    offset = 0
    pattern.each do |part_len|
      part = sha256_hex[offset, part_len]
      raise("invalid sha256_hex") if part.nil?
      parts << part
      offset += part_len
    end
    parts << sha256_hex
    parts
  end
end
