# encoding: utf-8
require 'multi_json'
require 'digest/sha1'
require 'pry'

module Rack
  class Webconsole
    # {Repl} is a Rack middleware acting as a Ruby evaluator application.
    #
    # In a nutshell, it evaluates a string in a {Sandbox} instance stored in an
    # evil global variable. Then, to keep the state, it inspects the local
    # variables and stores them in an instance variable for further retrieval.
    #
    class Repl
      @@request = nil
      @@tokens = {}

      class << self
        def clear_tokens
          @@tokens.each_pair do |k, v|
            @@tokens.delete(k) if v <= Time.now
          end
        end

        # Returns the autogenerated security token
        #
        # @return [String] the autogenerated token
        def token_valid? token
          clear_tokens
          @@tokens.keys.include?(token)
        end

        # Regenerates the token.
        def reset_token(app, env)
          clear_tokens
          token = Digest::SHA1.hexdigest("#{rand(36**8)}#{Time.now}")[4..20]
          @@tokens[token] = Time.now + 30 * 60
          token
        end

        # Returns the original request for inspection purposes.
        #
        # @return [Rack::Request] the original request
        def request
          @@request
        end

        # Sets the original request for inspection purposes.
        #
        # @param [Rack::Request] the original request
        def request=(request)
          @@request = request
        end

      end

      # Honor the Rack contract by saving the passed Rack application in an ivar.
      #
      # @param [Rack::Application] app the previous Rack application in the
      #   middleware chain.
      def initialize(app)
        @app = app

      end

      # Evaluates a string as Ruby code and returns the evaluated result as
      # JSON.
      #
      # It also stores the {Sandbox} state in a `$sandbox` global variable, with
      # its local variables.
      #
      # @param [Hash] env the Rack request environment.
      # @return [Array] a Rack response with status code 200, HTTP headers
      #   and the evaluated Ruby result.
      def call(env)
        status, headers, response = @app.call(env)

        req = Rack::Request.new(env)
        params = req.params

        return [status, headers, response] unless check_legitimate(req)

        hash = {}
        $pry_output ||= StringIO.new("")
        $pry_output.string = ""
        if $pry.nil?
          Pry.pager = false
          $pry = Pry.new(:output => $pry_output, :pager => false)
          Pry.initial_session_setup
        end
        pry = $pry
        
        # repl loop
        if pry.binding_stack.last
          target = Pry.binding_for(pry.binding_stack.last)
        else
          target = Pry.binding_for(TOPLEVEL_BINDING)
        end
        pry.repl_prologue(target) unless pry.binding_stack.last == target
        pry.inject_sticky_locals(target)
        code = params['query']
        hash[:prompt] = pry.select_prompt("", target) + Pry::Code.new(code).to_s
        got_output = false
        begin
          read_pipe, write_pipe = IO.pipe
          end_line = "~~~~~ rack-webconsole end output ~~~~~\n"

          thr = Thread.new do
            while true
              new_line = read_pipe.readline
              break if new_line == end_line
              $pry_output << new_line
            end
          end

          old_stdout = STDOUT.dup
          old_stderr = STDERR.dup
          STDOUT.reopen(write_pipe)
          STDERR.reopen(write_pipe)
          if !pry.process_command(code, "", target)
            result = target.eval(code, Pry.eval_path, Pry.current_line)
            got_output = true
          end
        rescue StandardError => e
          error_out = "Error: " + e.message
        ensure
          write_pipe << end_line

          thr.join
          read_pipe.close
          write_pipe.close
          STDOUT.reopen(old_stdout)
          STDERR.reopen(old_stderr)

        end

        if got_output
          pry.set_last_result(result, target, code)
          Pry.print.call($pry_output, result) if pry.should_print?
          # the below line doesn't work well with custom printers
          # (like awesome_print) for some reason
          #pry.show_result(result) if pry.should_print?
        end

        $pry_output.write(error_out) if error_out

        # cleanup (supposed to call when $pry is destroyed)
        # pry.repl_epilogue(target)
        
        hash[:result] = $pry_output.string
        response_body = MultiJson.encode(hash)
        headers = {}
        headers['Content-Type'] = 'application/json'
        headers['Content-Length'] = response_body.bytesize.to_s
        [200, headers, [response_body]]
      end

      private

      def check_legitimate(req)
        req.post? && Repl.token_valid?(req.params['token'])
      end
    end
  end
end
