require 'sinatra/base'
require 'sinatra/activerecord'

require 'attr_encrypted'
require 'active_support/all'

require 'shopify_api'
require 'omniauth-shopify-oauth2'

module Sinatra
  module Shopify
    module Methods

      # designed to be overridden
      def after_shopify_auth
      end

      # for the app bridge initializer
      def shop_origin
        "#{session[:shopify][:shop]}"
      end

      def shopify_session(&blk)
        return_to = request.path
        return_params = request.params

        if no_session?
          authenticate(return_to, return_params)

        elsif different_shop?
          clear_session
          authenticate(return_to, return_params)

        else
          shop_name = session[:shopify][:shop]
          token = session[:shopify][:token]
          activate_shopify_api(shop_name, token)
          yield shop_name
        end

      rescue ActiveResource::UnauthorizedAccess
        clear_session

        shop = Shop.find_by(name: shop_name)
        shop.token = nil
        shop.save

        redirect request.path
      end

      private

      def authenticate(return_to = '/', return_params = nil)
        session[:return_params] = return_params if return_params

        if shop_name = sanitized_shop_param(params)
          redirect "/login?shop=#{shop_name}"
        else
          redirect '/login'
        end
      end

      def base_url
        request_protocol = request.secure? ? 'https' : 'http'
        "#{request_protocol}://#{request.env['HTTP_HOST']}"
      end

      def no_session?
        !session.key?(:shopify)
      end

      def different_shop?
        params[:shop].present? && session[:shopify][:shop] != sanitized_shop_param(params)
      end

      def clear_session
        session.delete(:shopify)
        session.clear
      end

      def activate_shopify_api(shop_name, token)
        api_session = ShopifyAPI::Session.new(domain: shop_name, token: token, api_version: settings.api_version)
        ShopifyAPI::Base.activate_session(api_session)
      end

      def receive_webhook(&blk)
        return unless verify_shopify_webhook
        shop_name = request.env['HTTP_X_SHOPIFY_SHOP_DOMAIN']
        webhook_body = ActiveSupport::JSON.decode(request.body.read.to_s)
        yield shop_name, webhook_body
        status 200
      end

      def sanitized_shop_param(params)
        return unless params[:shop].present?
        name = params[:shop].to_s.strip
        name += '.myshopify.com' if !name.include?('myshopify.com') && !name.include?('.')
        name.gsub!('https://', '')
        name.gsub!('http://', '')

        u = URI("http://#{name}")
        u.host.ends_with?('.myshopify.com') ? u.host : nil
      end

      def verify_shopify_webhook
        data = request.body.read.to_s
        digest = OpenSSL::Digest.new('sha256')
        calculated_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, settings.shared_secret, data)).strip
        request.body.rewind

        if calculated_hmac == request.env['HTTP_X_SHOPIFY_HMAC_SHA256']
          true
        else
          puts 'Shopify Webhook verification failed!'
          false
        end
      end
    end

    def shopify_webhook(route, &blk)
      settings.webhook_routes << route
      post(route) do
        receive_webhook(&blk)
      end
    end

    def self.registered(app)
      app.helpers Shopify::Methods
      app.register Sinatra::ActiveRecordExtension

      app.set :database_file, File.expand_path('config/database.yml')

      app.set :erb, layout: :'layouts/application'
      app.set :views, File.expand_path('views')
      app.set :public_folder, File.expand_path('public')
      app.enable :inline_templates

      app.set :protection, except: :frame_options

      app.set :api_version, '2019-07'
      app.set :scope, 'read_products, read_orders'

      app.set :api_key, ENV['SHOPIFY_API_KEY']
      app.set :shared_secret, ENV['SHOPIFY_SHARED_SECRET']
      app.set :secret, ENV['SECRET']

      # csrf needs to be disabled for webhook routes
      app.set :webhook_routes, ['/uninstall']

      # add support for put/patch/delete
      app.use Rack::MethodOverride

      app.use Rack::Session::Cookie, key: 'rack.session',
                                     path: '/',
                                     secure: true,
                                     same_site: 'None',
                                     secret: app.settings.secret,
                                     expire_after: 60 * 30 # half an hour in seconds

      app.use Rack::Protection::AuthenticityToken, allow_if: lambda { |env|
        app.settings.webhook_routes.include?(env["PATH_INFO"])
      }

      OmniAuth.config.allowed_request_methods = [:post]

      app.use OmniAuth::Builder do
        provider :shopify,
          app.settings.api_key,
          app.settings.shared_secret,
          scope: app.settings.scope,
          setup: lambda { |env|
            shop = if env['REQUEST_METHOD'] == 'POST'
              env['rack.request.form_hash']['shop']
            else
              Rack::Utils.parse_query(env['QUERY_STRING'])['shop']
            end

            site_url = "https://#{shop}"
            env['omniauth.strategy'].options[:client_options][:site] = site_url
          }
      end

      ShopifyAPI::Session.setup(
        api_key: app.settings.api_key,
        secret: app.settings.shared_secret
      )

      app.get '/login' do
        erb :login, layout: false
      end

      app.get '/logout' do
        clear_session
        redirect '/login'
      end

      app.get '/auth/shopify/callback' do
        shop_name = params['shop']
        token = request.env['omniauth.auth']['credentials']['token']

        shop = Shop.find_or_initialize_by(name: shop_name)
        shop.token = token
        shop.save!

        session[:shopify] = {
          shop: shop_name,
          token: token
        }

        after_shopify_auth()

        return_params = session[:return_params]
        session.delete(:return_params)

        return_to = '/'
        return_to += "?#{return_params.to_query}" if return_params.present?

        redirect return_to
      end

      app.get '/auth/failure' do
        erb "<h1>Authentication Failed:</h1>
             <h3>message:<h3> <pre>#{params}</pre>", layout: false
      end
    end
  end

  register Shopify
end

class Shop < ActiveRecord::Base
  def self.secret
    @secret ||= ENV['SECRET']
  end

  attr_encrypted :token,
    key: secret,
    attribute: 'token_encrypted',
    mode: :single_iv_and_salt,
    algorithm: 'aes-256-cbc',
    insecure_mode: true

  validates_presence_of :name
  validates_presence_of :token, on: :create
end