# frozen_string_literal: true

module Matterhorn
  class MergeRequest < Matterhorn::Model
    include Matterhorn::Environment

    attr_accessor :req, :body, :hook_request, :github

    def initialize(req, body, github)
      @req = req
      @body = body
      @hook_request = JSON.parse(body)
      @github = github
    end

    # webhook request validation
    def action_commands
      @action_commands ||= ['merge', 'priority_merge', 'admin_merge', 'stop']
    end

    def valid_commands
      @valid_commands ||= (action_commands | ['priority', 'admin', 'sudo'])
    end

    def valid_commands_regex
      @valid_commands_regex ||= /(?:\+(#{valid_commands.join('|')}))/
    end

    validation_rule :github_signature do
      request_signature = req.headers['x-hub-signature'] || ''
      signature = 'sha1=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), WEBHOOK_SECRET, body)
      next true if Rack::Utils.secure_compare(signature, request_signature) || development? || test?
      validation_error :github_signature, 'invalid GitHub webhook signature'
    end

    validation_rule :github_event do
      # when we support more than one event type, expand this
      # next true if ['issue_comment'].include? req.headers['x-github-event']
      next true if req.headers['x-github-event'] == 'issue_comment'
      validation_error :github_event, "invalid GitHub webhook event `#{req.headers['x-github-event']}`"
    end

    validation_rule :github_action do
      case req.headers['x-github-event']
      when 'issue_comment'
        next true if hook_request['action'] == 'created'
        validation_error :github_action, "invalid GitHub webhook action `#{hook_request['action']}` for `#{req.headers['x-github-event']}`"
      else
        true
      end
    end

    validation_rule :project do
      next true unless project.nil?
      validation_error :project, "invalid project `#{project_name}`"
    end

    validation_rule :pull_request do
      next true unless !project.nil? && pull_request.nil?
      validation_error :pull_request, 'invalid GitHub issue - not a pull request'
    end

    validation_rule :commands do
      next true unless actions.empty?
      validation_error :commands, "no action command(s) requested #{commands.inspect}"
    end

    validation_rule :user do
      next true unless user['login'] == GITHUB_USERNAME || !team_member
      validation_error :user, "user #{user['login']} is not permitted to run #{commands.inspect}" if !team_member
      validation_error :user, 'cannot run commands as the GitHub Matterhorn service user' if user['login'] == GITHUB_USERNAME
    end

    validation_rule :deduplicate do
      next true if actions.include?('stop') || project.nil? # if project is invalid, queue lookup will fail most egregiously

      duplicate = false
      queue.priority_queue.each do |job|
        duplicate = true if job.args[1] == pull_request_number
      end
      queue.normal_queue.each do |job|
        duplicate = true if job.args[1] == pull_request_number
      end
      next true unless duplicate

      validation_error :deduplicate, "duplicate job for pull/#{pull_request_number} found"
    end

    # primary call to kick off the job request
    def run
      log.info "Request for pull/#{pull_request_number} on #{project_name}" unless project.nil? || pull_request.nil?
      if valid?
        process_job
        'Job received'
      else
        log.error validation_errors.inspect
        'Not a valid job request'
      end
    rescue StandardError => e
      log.error "#{project}: #{pull_request_number}\n#{e.message}\n#{e.backtrace.inspect}"
      github.add_comment(project, pull_request_number, "**MERGE QUEUE ERROR**: #{e.message}") unless silent_mode?
    end

    def process_job
      return log.info "pull/#{pull_request.number} on #{project} is already merged" if pull_request.merged
      return log.info "pull/#{pull_request.number} on #{project} is closed" if pull_request.state == 'closed'

      if actions.include?('stop')
        # +stop command means remove the job from the queue
        # +stop command takes precedence over +merge in the same request
        log.info "Removing merge job(s) for #{project} PR ##{pull_request_number} on behalf of #{user['login']}"
        delete_merge_jobs
        github.add_comment(project, pull_request_number, "**Stop complete** - any queued jobs for PR ##{pull_request_number} have been deleted. *Note: This will not impact jobs already in-flight.*") unless silent_mode?
      else
        # +merge, +priority_merge, +admin_merge - add the job to the correct queue
        log.info "Adding merge job to #{queue_name} for #{project} PR ##{pull_request_number} on behalf of #{user['login']}: #{commands.inspect}"
        merge_comment = notify_queue_position
        GitMergeWorker.set(queue: queue_name).perform_async(project, pull_request_number, {
          admin: admin?,
          merger: Matterhorn::Models::User.new(user['login'], user['ldap_dn']),
          comment_id: merge_comment&.id
        })
      end
    end

    def warn_invalid_user
      # github.add_comment(project, pull_request_number, "**WARN**: `+#{command}` requires that you be a member of one of these teams: #{allowed_teams}")
      log.warn "Access denied for #{user['login']} attempting to use command `+#{command}` on #{project} PR ##{pull_request_number}"
    end

    def notify_queue_position
      return if silent_mode?

      queue_length = queue.priority_queue.size
      queue_length += queue.normal_queue.size unless priority?
      queue_length += 1 unless queue.active_job.nil?

      if queue_length.zero?
        message = '**Merge job queued** - you are next!'
      elsif queue_length == 1
        message = '**Merge job queued** - there is 1 job ahead of you in the queue.'
      else
        message = "**Merge job queued** - there are #{queue_length} jobs ahead of you in the queue."
      end

      team = project_settings[:savant_team]
      message += "\nFollow your queue progression at https://savant.xarth.tv/#{team}/merge-queue" unless team.nil?

      github.add_comment(project, pull_request_number, message)
    end

    def delete_merge_jobs
      queue.normal_queue.each do |job|
        job.delete if job.args[1] == pull_request_number
      end
      queue.priority_queue.each do |job|
        job.delete if job.args[1] == pull_request_number
      end
    end

    def redis
      @redis ||= Redis.new(url: REDIS_URL)
    end

    def user
      hook_request['comment']['user']
    end

    def project_name
      hook_request['repository']['full_name'] if hook_request['repository']
    end

    def project
      @project ||= project_name if PROJECTS[:projects].key?(project_name)
    end

    def queue
      @queue ||= Matterhorn::Models::Queue.new(project)
    end

    def queue_name
      return queue.priority_queue_name if priority?
      queue.base_queue_name
    end

    def pull_request_number
      hook_request['issue']['number'] if hook_request['issue']
    end

    def pull_request
      @pull_request ||= github.pull(project, pull_request_number) if project && pull_request_number
    end

    def project_settings
      PROJECTS[:projects][project]
    end

    def silent_mode?
      project_settings[:silent_mode]
    end

    def commands
      @commands ||= begin
        hook_request['comment']['body'].scan(valid_commands_regex).flatten
      end
    end

    def actions
      @actions ||= commands & action_commands
    end

    def priority?
      actions.include?('priority_merge') || commands.include?('priority')
    end

    def admin?
      actions.include?('admin_merge') || commands.include?('admin')
    end

    def allowed_teams
      return nil if project.nil?
      @allowed_teams ||= begin
        if admin?
          project_settings[:admin_teams]
        elsif priority?
          project_settings[:priority_teams] || project_settings[:allowed_teams]
        else
          project_settings[:allowed_teams]
        end
      end
    end

    def teams
      @teams ||= Matterhorn::GitHub.teams_for_repository(project, allowed_teams, teams_cache_filter_id) unless allowed_teams.nil?
    end

    def teams_cache_filter_id
      @cache_filter_id ||= begin
        if admin?
          'admin'
        elsif priority?
          'priority'
        else
          'allowed'
        end
      end
    end

    def team_member
      @team_member ||= begin
        team_member = allowed_teams.nil? unless admin? # if no teams are specified, assume action is allowed for non-admin-merge
        teams&.each do |team|
          team_member ||= Matterhorn::GitHub.team_membership(team['id'], user['login'])
        end
        team_member
      end
    end
  end
end
