# frozen_string_literal: true

module MailBuilder
  # Takes in Issue Assignment IDs and Doc IDs and groups them together by repo
  #
  # The main purpose of this class is to make data easier to navigate for
  # rendering mail views, specifically the "daily triage" email.
  #
  # The main reason this exists is to minimize query time while rendering
  # mail views without sacraficing view readability.
  #
  # To access these elements you need to be inside of an `each` loop
  #
  #   group = GroupedIssuesDocs.new(User.last, assignment_ids: [99])
  #   group.each do |g|
  #     g.assignments # => [...]
  #     g.read_docs   # => [...]
  #     g.write_docs  # => [...]
  #     g.read_docs.each do |doc|
  #       g.comment_for_doc(doc) # => "..."
  #     end
  #   end
  #
  # There is a method `comment_for_doc` that will retrieve the documentation
  # comment object for a specific doc method.
  #
  # There are helper methods designed to make rendering views easier.
  # For example `GroupedIssuesDocs#actions` will return either "issues", "docs"
  # or "issues and docs" depending on the contents of the group.
  #
  # `GroupedIssuesDocs#any_docs?` returns true if there are docs in the group
  # `GroupedIssuesDocs#any_docs?` returns true if there are issues in the group
  #
  # Internally data is stored in a hash of hashes. The top level key is
  # the id of the repo subscription.
  #
  #   puts @sub_hashes
  #   {
  #     99 => {}
  #   }
  #
  # Each of these hashes has the following keys:
  #
  #   puts @sub_hashes.first.keys
  #   # => [:repo, :assignments, :read_docs, :write_docs]
  #
  # Each of these is an array except for repo.
  #
  # There is a helper hash @repo_id_to_sub that takes a repo id and
  # returns the subscription ID for that repo.
  #
  class GroupedIssuesDocs
    def initialize(user_id:, assignment_ids: [], read_doc_ids: [], write_doc_ids: [], random_seed: Random.new_seed)
      @active = false
      @sub_hashes = {}
      @repo_id_to_sub = {}
      @doc_comments_hash = {}
      @error_hash = Hash.new { raise "must call within each" }
      @active_hash = @error_hash

      @any_docs = read_doc_ids.present? || write_doc_ids.present?
      @any_issues = assignment_ids.present?

      ## Issue assignments
      assignments = IssueAssignment
        .where(id: assignment_ids)
        .includes(:issue)
        .select(:id, :repo_subscription_id, :issue_id)

      ## Docs
      docs = DocMethod
        .where(id: write_doc_ids + read_doc_ids)
        .select(:id, :repo_id, :line, :file, :path)

      ## Comments
      doc_comments = DocComment
        .where(doc_method_id: docs.map(&:id).uniq)
        .select(:comment, :doc_method_id)

      ## Subscriptions
      doc_repo_ids = docs.map(&:repo_id).uniq

      subscriptions = RepoSubscription
        .joins("LEFT OUTER JOIN issue_assignments ON issue_assignments.repo_subscription_id = repo_subscriptions.id")
        .where("issue_assignments.id in (?) or repo_id in (?)", assignment_ids, doc_repo_ids)
        .where(user_id: user_id)
        .select(:id, :repo_id)
        .includes(:repo)
        .all
        .shuffle(random: Random.new(random_seed))

      store_subscriptions!(subscriptions)
      store_assignments!(assignments)
      store_docs!(
        docs: docs,
        write_doc_ids: write_doc_ids,
        doc_comments: doc_comments
      )
    end

    def store_subscriptions!(subscriptions)
      subscriptions.each do |sub|
        @repo_id_to_sub[sub.repo.id] = sub.id

        @sub_hashes[sub.id] ||= {}
        @sub_hashes[sub.id][:repo] = sub.repo
        @sub_hashes[sub.id][:assignments] ||= []
        @sub_hashes[sub.id][:read_docs] ||= []
        @sub_hashes[sub.id][:write_docs] ||= []
      end
    end

    def store_assignments!(assignments)
      assignments.each do |assignment|
        @sub_hashes[assignment.repo_subscription_id][:assignments] << assignment
      end
    end

    def store_docs!(write_doc_ids:, docs:, doc_comments:)
      write_docs = []
      read_docs = []
      docs.each do |doc|
        if write_doc_ids.include?(doc.id)
          write_docs << doc
        else
          read_docs << doc
        end
      end

      doc_comments.each do |comment|
        @doc_comments_hash[comment.doc_method_id] = comment
      end

      write_docs.each do |doc|
        sub_id = @repo_id_to_sub.fetch(doc.repo_id)
        @sub_hashes[sub_id][:write_docs] << doc
      end

      read_docs.each do |doc|
        sub_id = @repo_id_to_sub.fetch(doc.repo_id)
        @sub_hashes[sub_id][:read_docs] << doc
      end
    end

    def each
      @sub_hashes.each do |_, hash|
        @active = true
        @active_hash = hash
        yield self
        @active = false
        @active_hash = @error_hash
      end
    end
    include Enumerable

    def repo
      @active_hash[:repo]
    end

    def assignments
      @active_hash[:assignments]
    end

    def read_docs
      @active_hash[:read_docs]
    end

    def write_docs
      @active_hash[:write_docs]
    end

    def any_docs
      return @any_docs if !@active
      read_docs.any? || write_docs.any?
    end

    def any_issues
      return @any_issues if !@active
      assignments.any?
    end

    alias_method :any_docs?, :any_docs
    alias_method :any_issues?, :any_issues

    def actions(delimiter: "and")
      return "issues" if !any_docs
      return "docs" if !any_issues
      "issues #{delimiter} docs"
    end

    def comment_for_doc(doc)
      @doc_comments_hash[doc.id]
    end

    def count
      @sub_hashes.length
    end
  end
end
