# Description:
#   Manage code review reminders
#
# Configuration:
#   see README.md
#
# Commands:
#   hubot help crs - display code review help

CodeReviews = require './CodeReviews'
CodeReview = require './CodeReview'
CodeReviewKarma = require './CodeReviewKarma'
msgRoomName = require './lib/msgRoomName'

module.exports = (robot) ->

  code_review_karma = new CodeReviewKarma robot
  code_reviews = new CodeReviews robot

  enqueue_code_review = (msg) ->
    # Ignore all bot messages
    if msg.message.user?.slack?.is_bot
      return
    url = msg.match[1]
    slug = code_reviews.matches_to_slug msg.match
    msgRoomName msg, (room_name) ->
      if slug and room_name
        cr = new CodeReview msg.message.user, slug, url, room_name, msg.message.room
        found = code_reviews.find_slug_index room_name, slug
        if found is false
          # 'Take' a code review for karma
          code_review_karma.incr_score msg.message.user.name, 'take'

          if (msg.match[5])? and msg.match[5].length
            notification_string = msg.match[5].replace /^\s+|\s+$/g, ""
          else
            notification_string = null
          # Add any extra info to the cr, seng extra notifications, and add it to the room_queue
          code_reviews.add_cr_with_extra_info(cr, msg, notification_string)

        else
          if code_reviews.room_queues[room_name][found].status != 'new'
            statusMsg = "#{code_reviews.room_queues[room_name][found].status}"
          else
            statusMsg = 'added'
          if code_reviews.room_queues[room_name][found].reviewer
            reviewerMsg = " and was #{statusMsg} by" +
            " @#{code_reviews.room_queues[room_name][found].reviewer}"
          else
            reviewerMsg = ''
          msg.send "*#{slug}* is already in the queue#{reviewerMsg}"
      else
        msg.send "Unable to add #{url} to queue. Please try again."

  # Respond to message with matching slug names
  #
  # @param slugs matching slugs
  # @param msg message to reply to
  # @return none
  send_be_more_specific = (slugs, msg) ->
    # Bold the slugs
    slugs = ("`#{slug}`" for slug in slugs)
    lastSlug = 'or ' + slugs.pop()
    slugs.push lastSlug
    msg.send "You're gonna have to be more specific: " + slugs.join(', ') + '?'

  # Return a single matching CR for slug match or alert the user to match status
  #
  # @param slugs matching slugs
  # @param msg message to reply to
  # @return none
  single_matching_cr = (slug_to_search_for, room_name, msg, status = false, no_reply = false) ->
    # search for matching slugs whether a fragment or full slug is provided
    found_crs = code_reviews.search_room_by_slug room_name, slug_to_search_for, status

    # no matches
    if found_crs.length is 0
      unless no_reply
        status_prs = if status then "#{status} " else ''
        msg.send "Sorry, I couldn't find any #{status_prs}PRs" +
        " in this room matching `#{slug_to_search_for}`."
      return
    # multiple matches
    else if found_crs.length > 1
      foundSlugs = for cr in found_crs
        cr.slug
      unless no_reply
        send_be_more_specific foundSlugs, msg
      return
    # There's a single matching slug in this room to redo
    else
      return found_crs[0]

  dequeue_code_review = (cr, reviewer, msg) ->
    if cr and cr.slug
      code_review_karma.incr_score reviewer, 'give'
      msg.send "Thanks, #{reviewer}! I removed *#{cr.slug}* from the code review queue."

  ###
  @command  hubot: help crs
  @desc     Display help docs for code review system
  ###
  robot.respond /help crs(?: --(flush))?$/i, id: 'crs.help', (msg) ->
    if ! code_reviews.help_text or (msg.match[1] and msg.match[1].toLowerCase() is 'flush')
      code_reviews.set_help_text()

    msg.send code_reviews.help_text

  ###
  @command  {GitHub pull request URL} [@user]
  @desc     Add PR to queue and (optionally) notify @user or #channel
  ###
  robot.hear code_reviews.pr_url_regex, enqueue_code_review

  ###
  @command  [hubot: ]on it
  @desc     Claim the oldest _new_ PR in the queue
  @command  [hubot: ]userName is on it
  @desc     Tell hubot that userName has claimed the oldest _new_ PR in the queue
  ###
  # Claim first PR in queue by directly addressing hubot
  robot.respond /(?:([-_a-z0-9]+) is )?on it/i, (msg) ->
    reviewer = msg.match[1] or msg.message.user.name
    msgRoomName msg, (room_name) ->
      if room_name
        cr = code_reviews.claim_first room_name, reviewer
        dequeue_code_review cr, reviewer, msg

  # Claim first PR in queue wihout directly addressing hubot
  # Note the this is a `hear` listener and previous is a `respond`
  robot.hear /^(?:([-_a-z0-9]+) is )?on it$/, (msg) ->
    # Ignore all bot messages
    if msg.message.user?.slack?.is_bot
      return
    reviewer = msg.match[1] or msg.message.user.name
    msgRoomName msg, (room_name) ->
      if room_name
        cr = code_reviews.claim_first room_name, reviewer
        dequeue_code_review cr, reviewer, msg

  ###
  @command  on *
  @desc     Claim all _new_ PRs
  ###
  robot.hear /^on \*$/i, (msg) ->
    # Ignore all bot messages
    if msg.message.user?.slack?.is_bot
      return
    msg.emote ":tornado2:"
    reviewer = msg.message.user.name
    msgRoomName msg, (room_name) ->
      if room_name
        until false is cr = code_reviews.claim_first room_name, reviewer
          dequeue_code_review cr, reviewer, msg

  ###
  @command [userName is ]on cool-repo/123
  @desc    Claim `cool-repo/123` if no one else has claimed it
  @command [userName is ]on cool
  @desc    Claim a _new_ PR whose slug matches `cool`
  ###
  robot.hear /^(?:([-_a-z0-9]+) is )?(?:on) ([-_\/a-z0-9]+|\d+|[-_\/a-z0-9]+\/\d+)$/i, (msg) ->
    # Ignore all bot messages
    if msg.message.user?.slack?.is_bot
      return
    reviewer = msg.match[1] or msg.message.user.name
    slug = msg.match[2]
    return if slug.toLowerCase() is 'it'

    msgRoomName msg, (room_name) ->
      if room_name
        unclaimed_cr = single_matching_cr(slug, room_name, msg, status = "new")
        if (unclaimed_cr)?
          code_reviews.claim_by_slug room_name, unclaimed_cr.slug, reviewer
          dequeue_code_review unclaimed_cr, reviewer, msg

        # none of the matches have "new" status
        else
          cr = single_matching_cr(slug, room_name, msg, status = false, no_output = true)
          # When someone attempts to claim a PR
          # that was already reviewed, merged, or closed outside of the queue
          if (cr)?
            response = "It looks like *#{cr.slug}* (@#{cr.user.name}) has already been #{cr.status}"
            msg.send response

  ###
  @command hubot (nm|ignore) cool-repo/123
  @desc    Delete `cool-repo/123` from queue regardless of status
  @command hubot (nm|ignore) cool
  @desc    Delete most recently added PR whose slug matches `cool` regardless of status
  ###
  robot.respond /(?:nm|ignore) ([-_\/a-z0-9]+|\d+|[-_\/a-z0-9]+\/\d+)$/i, (msg) ->
    slug = msg.match[1]
    return if slug.toLowerCase() is 'it'

    msgRoomName msg, (room_name) ->
      if room_name
        found_ignore_cr = single_matching_cr(slug, room_name, msg)
        if (found_ignore_cr)?
          code_reviews.remove_by_slug room_name, found_ignore_cr.slug
          #decrement scores
          code_review_karma.decr_score found_ignore_cr.user.name, 'take'
          if found_ignore_cr.reviewer
            code_review_karma.decr_score found_ignore_cr.reviewer, 'give'
          msg.send "Sorry for eavesdropping. I removed *#{found_ignore_cr.slug}* from the queue."
          return

  ###
  @command hubot: (nm|ignore)
  @desc    Delete most recently added PR from the queue regardless of status
  ###
  robot.respond /(?:\s*)(?:nm|ignore)(?:\s*)$/i, (msg) ->
    msgRoomName msg, (room_name) ->
      if room_name
        cr = code_reviews.remove_last_new room_name
        if cr and cr.slug
          code_review_karma.decr_score cr.user.name, 'take'
          if cr.reviewer
            code_review_karma.decr_score cr.reviewer, 'give'
          msg.send "Sorry for eavesdropping. I removed *#{cr.slug}* from the queue."
        else
          msg.send "There might not be a new PR to remove. Try specifying a slug."

  ###
  @command hubot: redo cool-repo/123
  @desc    Allow another review _without_ decrementing previous reviewer's score
  ###
  robot.respond /(?:redo)(?: ([-_\/a-z0-9]+|\d+|[-_\/a-z0-9]+\/\d+))/i, (msg) ->
    msgRoomName msg, (room_name) ->
      if room_name
        found_redo_cr = single_matching_cr(msg.match[1], room_name, msg)
        if (found_redo_cr)?
          index = code_reviews.find_slug_index room_name, found_redo_cr.slug
          code_reviews.reset_cr code_reviews.room_queues[room_name][index]
          msg.send "You got it, #{found_redo_cr.slug} is ready for a new review."

  ###
  @command hubot: (unclaim|reset) cool-repo/123
  @desc    Reset CR status to new/unclaimed _and_ decrement reviewer's score
  ###
  robot.respond /(unclaim|reset)(?: ([-_\/a-z0-9]+|\d+|[-_\/a-z0-9]+\/\d+))?/i, (msg) ->
    msgRoomName msg, (room_name) ->
      if room_name
        found_reset_cr = single_matching_cr(msg.match[2], room_name, msg)
        if (found_reset_cr)?
          # decrement reviewers CR score
          if found_reset_cr.reviewer
            code_review_karma.decr_score found_reset_cr.reviewer, 'give'

            index = code_reviews.find_slug_index room_name, found_reset_cr.slug
            code_reviews.reset_cr code_reviews.room_queues[room_name][index]
            msg.match[1] += 'ed' if msg.match[1].toLowerCase() is 'unclaim'
            msg.send "You got it, I've #{msg.match[1]} *#{found_reset_cr.slug}* in the queue."

  ###
  @command hubot: list crs
  @desc    List all _unclaimed_ CRs in the queue
  @command hubot: list [status] crs
  @desc    List CRs with matching optional status
  ###
  robot.respond /list(?: (all|new|claimed|approved|closed|merged))? CRs/i, (msg) ->
    status = msg.match[1] || 'new'
    msgRoomName msg, (room_name) ->
      if room_name
        code_reviews.send_list room_name, false, status

  # Flush all CRs in all rooms
  robot.respond /flush the cr queue, really really/i, (msg) ->
    code_reviews.flush_queues()
    msg.send "This house is clear"

  # Display JSON of all CR queues
  robot.respond /debug the cr queue ?(?:for #?([a-z0-9\-_]+))?$/i, (msg) ->
    if !msg.match[1]
      msg.send code_reviews.queues_debug_stats()
    else
      msg.send code_reviews.queues_debug_room(msg.match[1])

  # Mark a CR as approved or closed when webhook received from GitHub
  robot.router.post '/hubot/hubot-code-review', (req, res) ->
    # check header
    unless req.headers['x-github-event']
      res.statusCode = 400
      res.send 'x-github-event is required'
      return

    # Check if PR was approved (via emoji in issue_comment body)
    if req.headers['x-github-event'] is 'issue_comment' and
    req.body.comment.user.type != 'Bot' # Commenter is not a bot
      if ((process.env.HUBOT_CODE_REVIEW_EMOJI_APPROVE?) and
      process.env.HUBOT_CODE_REVIEW_EMOJI_APPROVE)
        if (code_reviews.emoji_regex.test(req.body.comment.body) or
        code_reviews.emoji_unicode_test(req.body.comment.body))
          code_reviews.approve_cr_by_url(
            req.body.issue.html_url,
            req.body.comment.user.login,
            req.body.comment.body
          )
          response = "issue_comment approved #{req.body.issue.html_url}"
        else
          code_reviews.comment_cr_by_url(
            req.body.issue.html_url,
            req.body.comment.user.login,
            req.body.comment.body
          )
          response = "issue_comment did not yet approve #{req.body.issue.html_url}"
      else
        code_reviews.comment_cr_by_url(
          req.body.issue.html_url,
          req.body.comment.user.login,
          req.body.comment.body
        )
        response = "issue_comment did not yet approve #{req.body.issue.html_url}"
    # Check if PR was merged or closed
    else if req.headers['x-github-event'] is 'pull_request'
      if req.body.action is 'closed'
        # update CRs
        status = if req.body.pull_request.merged then 'merged' else 'closed'
        updated = code_reviews.handle_close req.body.pull_request.html_url, status
        # build response message
        if updated.length
          response = "set status of #{updated[0].slug} to "
          rooms = for cr in updated
            "#{cr.status} in #{cr.room}"
          response += rooms.join(', ')
        else
          response = "#{req.body.pull_request.html_url} not found in any queue"
      else
        response = "#{req.body.pull_request.html_url} is still open"

    # Check if PR was approved via GitHub's Pull Request Review
    else if req.headers['x-github-event'] is 'pull_request_review' and
    req.body.review.user.type != 'Bot' # not a bot
      if req.body.action? and req.body.action is 'dismissed'
        response = "pull_request_review dismissed #{req.body.pull_request.html_url}"
        code_reviews.dismiss_cr_by_url(
          req.body.pull_request.html_url,
          req.body.review.user.login
        )
      else
        if req.body.review.state is 'approved'
          response = "pull_request_review approved #{req.body.pull_request.html_url}"
          code_reviews.approve_cr_by_url(
            req.body.pull_request.html_url,
            req.body.review.user.login,
            req.body.review.body
          )
        else if req.body.review.state is 'changes_requested'
          response = "pull_request_review changes requested #{req.body.pull_request.html_url}"

          # Send the changes requested comment to the submitter
          if req.body.review.body?
            code_reviews.comment_cr_by_url(
              req.body.pull_request.html_url,
              req.body.review.user.login,
              req.body.review.body
            )
          # Close up the PR and notify the submitter to resubmit when changes are made
          code_reviews.handle_close(
            req.body.pull_request.html_url,
            'changes_requested'
          )
        else
          response = "pull_request_review not yet approved #{req.body.pull_request.html_url}"
          if req.body.review.body?
            code_reviews.comment_cr_by_url(
              req.body.pull_request.html_url,
              req.body.review.user.login,
              req.body.review.body
            )
    else
      res.statusCode = 400
      response = "invalid x-github-event #{req.headers['x-github-event']}"

    # useful for testing
    res.send response

  # return for use in unit tests
  return code_reviews
