require 'redis'
require 'openssl'
require 'socket'
require 'http/2'
require 'active_support/ordered_hash'
require 'active_support/json'
require 'active_support/core_ext/hash/keys'
require 'base64'
require 'airbrake'
require 'logger'

module Rapnd
  class Daemon
    attr_accessor :redis, :host, :cert, :queue, :logger, :airbrake
    
    def initialize(options = {})
      options[:redis_host]  ||= 'localhost'
      options[:redis_port]  ||= '6379'
      options[:host]        ||= :sandbox
      options[:queue]       ||= 'rapnd_queue'
      options[:password]    ||= ''
      options[:log_level]   ||= Logger::DEBUG
      raise 'No cert provided!' unless options[:cert]
      
      Airbrake.configure { |config| config.api_key = options[:airbrake]; @airbrake = true; } if options[:airbrake]
      
      redis_options = { :host => options[:redis_host], :port => options[:redis_port] }
      redis_options[:password] = options[:redis_password] if options.has_key?(:redis_password)
      
      options[:host] = set_host(options[:host])
      
      @redis = Redis.new(redis_options)
      @queue = options[:queue]
      @cert = options[:cert]
      @host = options[:host]
      @logger = Logger.new(options[:log_stdout] ? STDOUT : "#{options[:dir]}/log/#{options[:queue]}.log")
      @logger.level = options[:log_level]
      @logger.info "Listening on queue: #{self.queue}"
    end
    
    def connect
      connect! if @sock == nil || @sock.closed? || @connection.closed?
    end
    
    def connect!
      @logger.info "Connecting to #{@host}..."
      @context      = OpenSSL::SSL::SSLContext.new
      @context.cert = OpenSSL::X509::Certificate.new(File.read(@cert))
      @context.key  = OpenSSL::PKey::RSA.new(File.read(@cert), @password)
      
      tcp           = TCPSocket.new(@host, 443)
      @sock         = OpenSSL::SSL::SSLSocket.new(tcp, @context)
      @sock.sync    = true
      @sock.connect
      
      @connection   = HTTP2::Client.new
      @connection.on(:frame) do |bytes|
        @logger.debug "I/O #{bytes.length} bytes..."
        @sock.print bytes
        @sock.flush
      end
      
      @logger.info 'Connected!'
      
      return @sock
    end
    
    def run!
      loop do
        begin
          message = @redis.blpop(self.queue, :timeout => 1)
          if message
            notification = Rapnd::Notification.new(JSON(message.last).symbolize_keys)
            self.connect
            headers, data = send_stream(notification)
            @logger.info "Sent to #{notification.device_token} #{@logger.level == Logger::DEBUG ? notification.json_payload : ''} -> [#{headers[':status']}] #{data}"
          end
        rescue Exception => e
          if e.class == Interrupt || e.class == SystemExit
            @logger.info "Shutting down..."
            exit(0)
          end
          self.connect
          if notification
            @logger.info "Trying again for: #{notification.json_payload}"
            send_stream(notification)
          end
          Airbrake.notify(e, {:environment_name => self.queue }) if @airbrake
          @logger.error "Encountered error: #{e}"
          if @sock.closed?
            @logger.error "Socket was closed, will try to reconnect next time."
          end
        end
      end
    end
    
    private
    
    # This method provides a simple way set host to Apple's predefined
    # hosts based on setting the `:host` option as a simple. The two options are
    # `:sandbox` and `:production`.
    def set_host(host)
      case host
      when :sandbox then 'api.sandbox.push.apple.com'
      when :production then 'api.push.apple.com'
      else host
      end
    end
    
    def send_stream(notification)
      # HTTP/2 Connection
      self.connect
      
      stream_closed = false
      stream = @connection.new_stream
      stream.on(:close) do
        @logger.debug 'Stream closed.'
        stream_closed = true
      end
      
      stream.on(:half_close) do
        @logger.debug 'Stream half-closed...'
      end
      
      @logger.debug 'Sending HTTP 2.0 request...'
      stream.headers(notification.http2_headers, end_stream: false)
      stream.data(notification.json_payload)
      
      headers = nil
      stream.on(:headers) { |h| headers = h }
      
      body = nil
      stream.on(:data) { |d| body = d }
      
      while !@sock.closed? && !@sock.eof?
        data = @sock.read_nonblock(1024)
        begin
          @connection << data
        rescue => e
          @logger.error "HTTP/2 Exception: #{e}, #{e.message} - closing socket."
          @sock.close
        end
        
        break if stream_closed
      end
      
      # Old Binary interface
      # result = tcp.write(notification.to_bytes)
      
      return headers.to_h, body
    end
  end
end
