class Issue < Note
  include Commentable
  include Eventable
  include InlineCommentable
  # FIXME - ISSUE/NOTE INHERITANCE
  # Commentable.allowed_types << base.name doesn't work for Issue because it's
  # an STI model, so we manually allow the class here.
  Commentable.allowed_types << 'Issue'
  InlineCommentable.allowed_types << 'Issue'

  include Subscribable
  Subscribable.allowed_types << 'Issue'

  include Taggable

  # -- Relationships --------------------------------------------------------
  has_many :evidence, dependent: :destroy
  has_many :affected, through: :evidence, source: :node

  # `has_many :activities` doesn't work as normal here, because we're not
  # using proper single-table inheritance. (By default it will search
  # for activities where trackable_type is "Note" instead of "Issue".) So
  # we need to override Issue#activities with a hack.
  #
  # NOTE: doing it this way means we don't get some of the methods that
  # are automatically generated by has_many, such as `activity_ids`. Some
  # of the other methods listed at http://guides.rubyonrails.org/association_basics.html#has-many-association-reference
  # might not work as intended either. Proceed with caution.
  #
  # FIXME - ISSUE/NOTE INHERITANCE
  def activities(*params)
    Activity.where(trackable_type: 'Issue', trackable_id: self.id)
  end

  # `has_many :comments` doesn't work as normal here, because we're not
  # using proper single-table inheritance. (By default it will search
  # for comments where commentable_type is "Note" instead of "Issue".) So
  # we need to override Issue#comments with a hack.
  #
  # NOTE: doing it this way means we don't get some of the methods that
  # are automatically generated by has_many, such as `comment_ids`. Some
  # of the other methods listed at http://guides.rubyonrails.org/association_basics.html#has-many-association-reference
  # might not work as intended either. Proceed with caution.
  #
  # FIXME - ISSUE/NOTE INHERITANCE
  def comments(*params)
    Comment.where(commentable_type: 'Issue', commentable_id: self.id)
  end

  # `has_many :inline_threads` doesn't work as normal here, because
  # we're not using proper single-table inheritance. (By default it will search
  # for threads where commentable_type is "Note" instead of "Issue".) So
  # we need to override Issue#inline_threads with a hack.
  #
  # FIXME - ISSUE/NOTE INHERITANCE
  def inline_threads(*params)
    InlineThread.where(commentable_type: 'Issue', commentable_id: self.id)
  end

  # `has_many :subscriptions` doesn't work as normal here, because we're not
  # using proper single-table inheritance. (By default it will search
  # for comments where subscribable_type is "Note" instead of "Issue".) So
  # we need to override Issue#subscriptions with a hack.
  #
  # NOTE: doing it this way means we don't get some of the methods that
  # are automatically generated by has_many, such as `subscription_ids`. Some
  # of the other methods listed at http://guides.rubyonrails.org/association_basics.html#has-many-association-reference
  # might not work as intended either. Proceed with caution.
  #
  # FIXME - ISSUE/NOTE INHERITANCE
  def subscriptions(*params)
    Subscription.where(subscribable_type: 'Issue', subscribable_id: self.id)
  end

  # -- Callbacks ------------------------------------------------------------
  before_validation do
    self.category = Category.issue unless self.category
  end

  # -- Validations ----------------------------------------------------------

  # -- Scopes ---------------------------------------------------------------

  # -- Class Methods --------------------------------------------------------

  # Create a hash with all issues where the keys correspond to the field passed
  # as an argument
  def self.all_issues_by_field(field)
    # we don't memoize it because we want it to reflect recently added Issues
    issues_map = Issue.all.map do |issue|
      [issue.fields[field], issue]
    end
    Hash[issues_map]
  end

  def self.autogenerate_from(evidence)
    create(
      text: "#[Title]#\nAuto-generated Issue - #{Time.now}",
      node: evidence.project.issue_library,
      author: evidence.author
    )
  end

  # -- Instance Methods -----------------------------------------------------

  def <=>(other)
    self.title <=> other.title
  end

  # This method groups all the available evidence associated with this Issue
  # into a Hash where the keys are the nodes. E.g.:
  # {
  #   <node 1> => [<evidence 1.1>, <evidence 1.2>],
  #   <node 2> => [<evidence 2.1>]
  # }
  #
  # This is useful in a number of views to present or hide information about
  # all the instances for a given issue and node/host.
  def evidence_by_node()
    results = Hash.new { |h, k| h[k] = [] }

    self.evidence.includes(:node).each do |evidence|
      results[evidence.node] << evidence
    end

    # This sorts nodes by IP address. Non-IPs appear first
    results.sort_by do |node, _|
      node.label.split('.').map(&:to_i)
    end
  end

  # Returns a hash that will be used as part of event notification workflows.
  def local_event_payload
    {
      author: author,
      project: {
        id: project.id,
        name: project.name
      },
      state: state,
      tag: tags.first,
      fields: fields,
      title: title
    }
  end

  # Move all Evidence attached to issues with ids in issue_ids
  # array to current issue.
  # Then delete those issues without Evidence.
  # Returns the number of issues affected.
  def merge(issue_ids)
    merged = 0
    issue_ids = [issue_ids] if issue_ids.is_a?(Integer)

    # check all specified ids are integers
    return merged unless issue_ids.all? { |i| i.is_a? Integer }

    # assert current id is not there
    issue_ids -= [id]

    # merge
    if issue_ids.any?
      self.transaction do
        Evidence.where(issue_id: issue_ids).update_all(issue_id: id)
        merged = Issue.where(id: issue_ids).destroy_all.count
      end
    end

    merged
  end

  # This method inspects the issues' Tag field and if present tags the issue
  # accordingly.
  def tag_from_field_content!
    # If the Issue already has tags (e.g. from the HTML form), or if it doesn't
    # have a Tags field, bail.
    return if tags.any?
    return unless fields['Tags'].present?

    # For now we just care about the first tag
    if (tag_name = fields['Tags'].split(',').first)
      self.tag_list = tag_name
      self.save!
    end
  end

  ActiveSupport.run_load_hooks(:issue_model, self)
end
