##########
# -> File: /site_hook/lib/site_hook/webhook.rb
# -> Project: site_hook
# -> Author: Ken Spencer <me@iotaspencer.me>
# -> Last Modified: 1/10/2018 21:35:44
# -> Copyright (c) 2018 Ken Spencer
# -> License: MIT
##########
require 'rack'
require 'site_hook/logger'
require 'json'
require 'grape'
require 'grape-route-helpers'

module SiteHook
  class Server < Grape::API
    version nil
    prefix ''
    format :json

    #
    # @param [String] body JSON String of body
    # @param [String] sig Signature or token from git service
    # @param [String] secret User-defined verification token
    # @param [Boolean] plaintext Whether the verification is plaintext
    # @param [String] service service name
    def self.verified?(body, sig, secret, plaintext:, service:)
      if plaintext
        sig == secret
      else
        case service
        when 'gogs'
          if sig == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, body.chomp)
            SiteHook::Log.app.debug "Secret verified: #{sig} === #{OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, body)}"
            true
          end
        when 'github'
          if sig == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, body.chomp)
            SiteHook::Log.app.debug "Secret verified: #{sig} === #{OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, body)}"
            true
          end
        else
          # This shouldn't happen
        end
      end
    end

    helpers do
      def halt(status, message, headers)
        error!(message, status, headers)
      end
    end
    APPLICATION_JSON = 'application/json'
    before do
      remote_addr      = request.env['REMOTE_ADDR']
      cf_connecting_ip = request.env.fetch('HTTP_CF_CONNECTING_IP', nil)
      ip               = cf_connecting_ip || remote_addr
      SiteHook::Log.access.log "#{ip} - #{request.request_method} #{request.path}"
    end
    after do
      SiteHook::Log.access.log "#{status}"
    end
    resource :webhook do
      route_param :hook_name do
        get do
          STDOUT.puts params[:hook_name]
          project = SiteHook::Config.projects.find_project(StrExt.mkvar(params[:hook_name]))
          if project.nil?
            {message: 'project not found or private', status: 1, project: {}}
          else
            project_obj = {}
            %i[src dst repo host].each do |option|
              project_obj[option] = project.instance_variable_get(StrExt.mkvar(option))
            end
            {project: project_obj}
          end
        end
        post do
          service = nil
          request.body.rewind
          req_body = request.body.read
          project  = SiteHook::Config.projects.get(StrExt.mkvar(params[:hook_name]))
          CONTENT_TYPE   = 'Content-Type'
          if project == :not_found
            halt 404, {message: 'no such project', status: 1}.to_json, {CONTENT_TYPE => APPLICATION_JSON}
          elsif project == :no_projects
            halt 500, {message: 'no projects defined', status: 2}.to_json, {CONTENT_TYPE => APPLICATION_JSON}
          end
          plaintext = false
          signature = nil
          event     = nil
          gogs      = request.env.fetch('HTTP_X_GOGS_EVENT', nil)
          unless gogs.nil?
            event = 'push' if gogs == 'push'
          end
          github = request.env.fetch('HTTP_X_GITHUB_EVENT', nil)
          unless github.nil?
            event = 'push' if github == 'push'
          end
          gitlab = request.env.fetch('HTTP_X_GITLAB_EVENT', nil)
          unless gitlab.nil?
            event = 'push' if gitlab == 'push'
          end

          events = {'github' => github, 'gitlab' => gitlab, 'gogs' => gogs}
          if events['github'] && events['gogs']
            events['github'] = nil
          end
          events_m_e = events.values.one?
          case events_m_e
          when true
            event   = 'push'
            service = events.select { |_key, value| value }.keys.first
          when false
            halt 400, {message: 'events are mutually exclusive', status: 'failure'}.to_json, {CONTENT_TYPE => APPLICATION_JSON}

          else
            halt 400, {'status': 'failure', 'message': 'something weird happened'}, {CONTENT_TYPE => APPLICATION_JSON}
          end
          if event != 'push' && event.nil?
            halt 400, {message: 'no event header', status: 'failure'}.to_json, {CONTENT_TYPE => APPLICATION_JSON}
          end
          case service
          when 'gitlab'
            signature = request.env.fetch('HTTP_X_GITLAB_TOKEN', '')
            plaintext = true
          when 'github'
            signature = request.env.fetch('HTTP_X_HUB_SIGNATURE', '').sub!(/^sha1=/, '')
            plaintext = false

          when 'gogs'
            signature = request.env.fetch('HTTP_X_GOGS_SIGNATURE', '')
            plaintext = false
          else
            # This shouldn't happen
          end
          if Server.verified?(req_body.to_s, signature, project.hookpass, plaintext: plaintext, service: service)
            SiteHook::Log.build.info 'Building...'
            begin
              jekyll_status = SiteHook::Senders::Jekyll.build(project.src, project.dst, SiteHook::Log.build, options: {config: project.config})
              case jekyll_status[:status]

              when 0
                status 200
                {status: 'success'}
              when -1, -2, -3
                halt 400, {'status': 'exception', error: jekyll_status.fetch(:message).to_s}, {CONTENT_TYPE => APPLICATION_JSON}
              else
                # This shouldn't happen
              end
            rescue => e
              halt 500, {'status': 'exception', error: e.to_s}, {CONTENT_TYPE => APPLICATION_JSON}
            end
          else
            halt 403, {message: 'incorrect secret', 'status': 'failure'}.to_json, {CONTENT_TYPE => APPLICATION_JSON}
          end
        end
      end
    end

    resource :webhooks do
      get do
        SiteHook::Config.projects.to_h.reject do |_hook, options|
          options.fetch(:private)
        end

      end

    end
  end
end