# frozen_string_literal: true

require "json"
require "base64"
require "securerandom"

require "specwrk/store/base_adapter"

module Specwrk
  class Store
    class FileAdapter < BaseAdapter
      EXT = ".wrk.json"

      @work_queue = Queue.new
      @threads = []

      class << self
        def with_lock(uri, key)
          lock_file_path = File.join(uri.path, "#{key}.lock").tap do |path|
            FileUtils.mkdir_p(uri.path)
          end

          lock_file = File.open(lock_file_path, "a")

          Thread.pass until lock_file.flock(File::LOCK_EX)

          yield
        ensure
          lock_file.flock(File::LOCK_UN)
        end

        def schedule_work(&blk)
          start_threads!
          @work_queue.push blk
        end

        def start_threads!
          return if @threads.length.positive?

          Array.new(ENV.fetch("SPECWRK_THREAD_COUNT", "4").to_i) do
            @threads << Thread.new do
              loop do
                @work_queue.pop.call
              end
            end
          end
        end
      end

      def [](key)
        content = read(key.to_s)
        return unless content

        JSON.parse(content, symbolize_names: true)
      end

      def []=(key, value)
        key_string = key.to_s
        if value.nil?
          delete(key_string)
        else
          filename = filename_for_key(key_string)
          write(filename, JSON.generate(value))
        end
      end

      def keys
        known_key_pairs.keys
      end

      def clear
        FileUtils.rm_rf(path)
        FileUtils.mkdir_p(path)
      end

      def delete(*keys)
        filenames = keys.map { |key| filename_for_key key }.compact

        FileUtils.rm_f(filenames)
      end

      def merge!(h2)
        multi_write(h2)
      end

      def multi_read(*read_keys)
        result_queue = Queue.new

        read_keys.each do |key|
          self.class.schedule_work do
            result_queue.push([key.to_s, read(key)])
          end
        end

        Thread.pass until result_queue.length == read_keys.length

        results = {}
        until result_queue.empty?
          result = result_queue.pop
          next if result.last.nil?

          results[result.first] = JSON.parse(result.last, symbolize_names: true)
        end

        read_keys.map { |key| [key.to_s, results[key.to_s]] if results.key?(key.to_s) }.compact.to_h # respect order requested in the returned hash
      end

      def multi_write(hash)
        result_queue = Queue.new

        hash_with_filenames = hash.map { |key, value| [key.to_s, [filename_for_key(key.to_s), value]] }.to_h
        hash_with_filenames.each do |key, (filename, value)|
          content = JSON.generate(value)

          self.class.schedule_work do
            result_queue << write(filename, content)
          end
        end

        Thread.pass until result_queue.length == hash.length
      end

      def empty?
        Dir.empty? path
      end

      private

      def write(filename, content)
        tmp_filename = [filename, SecureRandom.uuid, "tmp"].join(".")

        File.binwrite(tmp_filename, content)

        FileUtils.mv tmp_filename, filename
        true
      end

      def read(key)
        filename = filename_for_key key
        File.read(filename)
      rescue Errno::ENOENT
        nil
      end

      def filename_for_key(key)
        File.join(
          path,
          encode_key(key)
        ) + EXT
      end

      def path
        @path ||= File.join(uri.path, scope).tap do |full_path|
          FileUtils.mkdir_p(full_path)
        end
      end

      def encode_key(key)
        Base64.urlsafe_encode64(key).delete("=")
      end

      def decode_key(key)
        encoded_key_part = File.basename(key).delete_suffix(EXT)
        padding_count = (4 - encoded_key_part.length % 4) % 4

        Base64.urlsafe_decode64(encoded_key_part + ("=" * padding_count))
      end

      def known_key_pairs
        Dir.entries(path).sort.map do |filename|
          next if filename.start_with? "."
          next unless filename.end_with? EXT

          file_path = File.join(path, filename)
          [decode_key(filename), file_path]
        end.compact.to_h
      end
    end
  end
end
