require 'sinatra'
require 'logger'
require 'json'
require 'openssl'
require 'octokit'
require 'jwt'
require 'time' # This is necessary to get the ISO 8601 representation of a Time object

set :port, 3000

#
#
# This is a customized server for the GitHub App you can build by following
# https://developer.github.com/build-your-first-github-app.
#
#

class GHAapp < Sinatra::Application

# Never, ever, hardcode app tokens or other secrets in your code!
# Always extract from a runtime source, like an environment variable.


# Notice that the private key must be in PEM format, but the newlines should be stripped and replaced with
# the literal `\n`. This can be done in the terminal as such:
# export GITHUB_PRIVATE_KEY=`awk '{printf "%s\\n", $0}' private-key.pem`
  PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # convert newlines

# You set the webhook secret when you create your app. This verifies that the webhook is really coming from GH.
  WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET']

# Get the app identifier—an integer—from your app page after you create your app. This isn't actually a secret,
# but it is something easier to configure at runtime.
  APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER']

# You need to authenticate to the REST API to do much of anything

########## Configure Sinatra
#
# Let's turn on verbose logging during development
#
  configure :development do
    set :logging, Logger::DEBUG
  end


########## Before each request to our app
#
# Before each request to our app, we want to instantiate an Octokit client. Doing so requires that we construct a JWT.
# https://jwt.io/introduction/
# We have to also sign that JWT with our private key, so GitHub can be sure that
#  a) it came from us
#  b) it hasn't been altered by a malicious third party
#
  before do
    payload = {
        # The time that this JWT was issued, _i.e._ now.
        iat: Time.now.to_i,

        # How long is the JWT good for (in seconds)?
        # Let's say it can be used for 10 minutes before it needs to be refreshed.
        # TODO we don't actually cache this token, we regenerate a new one every time!
        exp: Time.now.to_i + (10 * 60),

        # Your GitHub App's identifier number, so GitHub knows who issued the JWT, and know what permissions
        # this token has.
        iss: APP_IDENTIFIER
    }

    # Cryptographically sign the JWT
    jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')

    # Create the Octokit client, using the JWT as the auth token.
    # Notice that this client will _not_ have sufficient permissions to do many interesting things!
    # The helper methods below include one that generates an installation token (using the JWT) and
    # instantiates a new client object.
    @client ||= Octokit::Client.new(bearer_token: jwt)

  end




########## Events
#
# This is the webhook endpoint that GH will call with events, and hence where we will do our event handling
#

  post '/' do
    request.body.rewind
    payload_raw = request.body.read # We need the raw text of the body to check the webhook signature
    begin
      payload = JSON.parse payload_raw
    rescue
      payload = {}
    end

    # Check X-Hub-Signature to confirm that this webhook was generated by GitHub, and not a malicious third party.
    # The way this works is: We have registered with GitHub a secret, and we have stored it locally in WEBHOOK_SECRET.
    # GitHub will cryptographically sign the request payload with this secret. We will do the same, and if the results
    # match, then we know that the request is from GitHub (or, at least, from someone who knows the secret!)
    # If they don't match, this request is an attack, and we should reject it.
    # The signature comes in with header x-hub-signature, and looks like "sha1=123456"
    # We should take the left hand side as the signature method, and the right hand side as the
    # HMAC digest (the signature) itself.
    their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1='
    method, their_digest = their_signature_header.split('=')
    our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, payload_raw)
    halt 401 unless their_digest == our_digest

    # Determine what kind of event this is, and take action as appropriate
    # TODO we assume that GitHub will always provide an X-GITHUB-EVENT header in this case, which is a reasonable
    #      assumption, however we should probably be more careful!
    logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}"
    logger.debug "----         action #{payload['action']}" unless payload['action'].nil?

    case request.env['HTTP_X_GITHUB_EVENT']
    when 'issues'
      authenticate_installation(payload)
      if payload['action'] === 'opened'
        handle_issue_opened_event(payload)
      end
    end

    'ok'  # we have to return _something_ ;)
  end


########## Helpers
#
# These functions are going to help us do some tasks that we don't want clogging up the happy paths above, or
# that need to be done repeatedly. You can add anything you like here, really!
#

  helpers do

    # Authenticate each installation of the app in order to run API operations
    def authenticate_installation(payload)
      installation_id = payload['installation']['id']
      installation_token = @client.create_app_installation_access_token(installation_id)[:token]
      @bot_client = Octokit::Client.new(bearer_token: installation_token)
    end

    # When an issue is opened, add a label
    def handle_issue_opened_event(payload)
      repo = payload['repository']['full_name']
      issue_number = payload['issue']['number']
      @bot_client.add_labels_to_an_issue(repo, issue_number, ['needs-response'])
    end

  end


# Finally some logic to let us run this server directly from the commandline, or with Rack
# Don't worry too much about this code ;) But, for the curious:
# $0 is the executed file
# __FILE__ is the current file
# If they are the same—that is, we are running this file directly, call the Sinatra run method
  run! if __FILE__ == $0
end
