class Conversations::UnreadCounts::Store
  extend ::Conversations::UnreadCounts::StoreKeys

  class << self
    def base_ready?(account_id)
      Redis::Alfred.exists?(base_ready_key(account_id))
    end

    def assignment_ready?(account_id)
      Redis::Alfred.exists?(assignment_ready_key(account_id))
    end

    def mark_base_ready!(account_id)
      Redis::Alfred.set(base_ready_key(account_id), Time.current.to_i, ex: Conversations::UnreadCounts::READY_TTL)
    end

    def mark_assignment_ready!(account_id)
      Redis::Alfred.set(assignment_ready_key(account_id), Time.current.to_i, ex: Conversations::UnreadCounts::READY_TTL)
    end

    def clear_account!(account_id)
      delete_matching("#{account_prefix(account_id)}::*")
    end

    def clear_assignment!(account_id)
      assignment_key_patterns(account_id).each { |pattern| delete_matching(pattern) }
    end

    def add_base_membership(account_id:, inbox_id:, label_ids:, conversation_id:, team_id: nil)
      add_to_sets(base_keys(account_id, inbox_id, label_ids, team_id), conversation_id)
    end

    def remove_base_membership(account_id:, inbox_ids:, label_ids:, conversation_id:, team_ids: [])
      keys = Array(inbox_ids).flat_map { |inbox_id| removable_base_keys(account_id, inbox_id, label_ids, team_ids) }
      remove_from_sets(keys, conversation_id)
    end

    def add_assignment_membership(account_id:, conversation_id:, **membership)
      add_to_sets(
        assignment_keys(account_id, membership[:inbox_id], membership[:label_ids], membership[:assignee_id], membership[:team_id]),
        conversation_id
      )
    end

    def remove_assignment_membership(account_id:, conversation_id:, **membership)
      keys = Array(membership[:inbox_ids]).flat_map do |inbox_id|
        Array(membership[:assignee_ids]).flat_map do |assignee_id|
          removable_assignment_keys(account_id, inbox_id, membership[:label_ids], assignee_id, membership[:team_ids])
        end
      end
      remove_from_sets(keys, conversation_id)
    end

    def add_memberships(account_id:, memberships:, assignment: false)
      return if memberships.blank?

      Redis::Alfred.pipelined do |pipeline|
        memberships.each do |membership|
          keys = if assignment
                   assignment_keys(account_id, membership[:inbox_id], membership[:label_ids], membership[:assignee_id], membership[:team_id])
                 else
                   base_keys(account_id, membership[:inbox_id], membership[:label_ids], membership[:team_id])
                 end

          keys.each do |key|
            pipeline.sadd(key, membership[:conversation_id])
            pipeline.expire(key, Conversations::UnreadCounts::SET_TTL)
          end
        end
      end
    end

    def counts_for_keys(keys)
      keys = keys.compact_blank
      return {} if keys.blank?

      counts = Redis::Alfred.pipelined do |pipeline|
        keys.each { |key| pipeline.scard(key) }
      end
      keys.zip(counts).to_h
    end

    def memberships_for_keys(keys, conversation_id)
      keys = keys.compact_blank
      return {} if keys.blank?

      memberships = Redis::Alfred.pipelined do |pipeline|
        keys.each { |key| pipeline.sismember(key, conversation_id) }
      end
      keys.zip(memberships.map { |membership| membership == true || membership == 1 }).to_h
    end

    private

    def base_ready_key(account_id)
      format(Redis::Alfred::UNREAD_CONVERSATIONS_BASE_READY, account_id: account_id)
    end

    def assignment_ready_key(account_id)
      format(Redis::Alfred::UNREAD_CONVERSATIONS_ASSIGNMENT_READY, account_id: account_id)
    end

    def account_prefix(account_id)
      format(Redis::Alfred::UNREAD_CONVERSATIONS_ACCOUNT_PREFIX, account_id: account_id)
    end

    def base_keys(account_id, inbox_id, label_ids, team_id = nil)
      keys = [inbox_key(account_id, inbox_id)] + Array(label_ids).map { |label_id| label_inbox_key(account_id, label_id, inbox_id) }
      keys << team_inbox_key(account_id, team_id, inbox_id) if team_id.present?
      keys
    end

    def removable_base_keys(account_id, inbox_id, label_ids, team_ids)
      keys = base_keys(account_id, inbox_id, label_ids)
      keys.concat(Array(team_ids).compact_blank.map { |team_id| team_inbox_key(account_id, team_id, inbox_id) })
    end

    def assignment_keys(account_id, inbox_id, label_ids, assignee_id, team_id = nil)
      keys = assignment_keys_without_team(account_id, inbox_id, label_ids, assignee_id)
      keys << team_assignment_key(account_id, team_id, inbox_id, assignee_id) if team_id.present?
      keys
    end

    def removable_assignment_keys(account_id, inbox_id, label_ids, assignee_id, team_ids)
      keys = assignment_keys_without_team(account_id, inbox_id, label_ids, assignee_id)
      keys.concat(Array(team_ids).compact_blank.map { |team_id| team_assignment_key(account_id, team_id, inbox_id, assignee_id) })
    end

    def assignment_keys_without_team(account_id, inbox_id, label_ids, assignee_id)
      return assignee_keys(account_id, inbox_id, label_ids, assignee_id) if assignee_id.present?

      unassigned_keys(account_id, inbox_id, label_ids)
    end

    def assignee_keys(account_id, inbox_id, label_ids, assignee_id)
      [inbox_assignee_key(account_id, inbox_id, assignee_id)] +
        Array(label_ids).map { |label_id| label_inbox_assignee_key(account_id, label_id, inbox_id, assignee_id) }
    end

    def unassigned_keys(account_id, inbox_id, label_ids)
      [inbox_unassigned_key(account_id, inbox_id)] +
        Array(label_ids).map { |label_id| label_inbox_unassigned_key(account_id, label_id, inbox_id) }
    end

    def team_assignment_key(account_id, team_id, inbox_id, assignee_id)
      return team_inbox_assignee_key(account_id, team_id, inbox_id, assignee_id) if assignee_id.present?

      team_inbox_unassigned_key(account_id, team_id, inbox_id)
    end

    def add_to_sets(keys, conversation_id)
      write_to_sets(keys) { |pipeline, key| pipeline.sadd(key, conversation_id) }
    end

    def remove_from_sets(keys, conversation_id)
      keys = keys.compact_blank
      return false if keys.blank?

      results = Redis::Alfred.pipelined do |pipeline|
        keys.each do |key|
          pipeline.srem(key, conversation_id)
          pipeline.expire(key, Conversations::UnreadCounts::SET_TTL)
        end
      end
      results.each_slice(2).any? { |removed, _| removed == true || (removed.respond_to?(:to_i) && removed.to_i.positive?) }
    end

    def write_to_sets(keys)
      keys = keys.compact_blank
      return if keys.blank?

      Redis::Alfred.pipelined do |pipeline|
        keys.each do |key|
          yield(pipeline, key)
          pipeline.expire(key, Conversations::UnreadCounts::SET_TTL)
        end
      end
    end

    def delete_matching(pattern)
      Redis::Alfred.scan_each(match: pattern, count: 1000) do |key|
        Redis::Alfred.delete(key)
      end
    end

    def assignment_key_patterns(account_id)
      prefix = account_prefix(account_id)
      [
        assignment_ready_key(account_id),
        "#{prefix}::INBOX::*::UNASSIGNED",
        "#{prefix}::INBOX::*::ASSIGNEE::*",
        "#{prefix}::LABEL::*::INBOX::*::UNASSIGNED",
        "#{prefix}::LABEL::*::INBOX::*::ASSIGNEE::*",
        "#{prefix}::TEAM::*::INBOX::*::UNASSIGNED",
        "#{prefix}::TEAM::*::INBOX::*::ASSIGNEE::*"
      ]
    end
  end
end
