module Jets::Commands::Call
  class Caller
    include Jets::AwsServices

    def initialize(provided_function_name, event, options={})
      @options = options
      @guess = @options[:guess].nil? ? true : @options[:guess]

      @provided_function_name = provided_function_name
      @event = event

      @invocation_type = options[:invocation_type] || "RequestResponse"
      @log_type = options[:log_type] || "Tail"
      @qualifier = @qualifier
    end

    def function_name
      if @provided_function_name == 'controller'
        "#{Jets.project_namespace}-controller"
      elsif @provided_function_name.starts_with?(Jets.project_namespace)
        @provided_function_name # fully qualified function name
      elsif @guess
        ensure_guesses_found! # possibly exits here
        guesser.function_name # guesser adds namespace already
      else
        [Jets.project_namespace, @provided_function_name].join('-')
      end
    end

    def run
      @options[:local] ? local_run : remote_run
    end

    # With local mode there is no way to bypass the guesser
    def local_run
      puts "Local mode enabled!"
      ensure_guesses_found! # possibly exits here
      klass = guesser.class_name.constantize
      # Example:
      #   PostsController.process(event, context, meth)
      event = JSON.load(transformed_event) || {} # transformed_event is JSON text String

      fun = Jets::Poly.new(klass, guesser.method_name)
      result = fun.run(event) # check the logs for polymorphic function errors
      # Note: even though data might not always be json, the JSON.dump does a
      # good job of not bombing, so always calling it to simplify code.

      text = Jets::Util.normalize_result(result)
      STDOUT.puts text
    end

    def remote_run
      puts "Calling lambda function #{function_name} on AWS" unless @options[:mute]
      return if @options[:noop]

      options = {
        # client_context: client_context,
        function_name: function_name,
        invocation_type: @invocation_type, # "Event", # RequestResponse
        log_type: @log_type, # pretty sweet
        payload: transformed_event, # "fileb://file-path/input.json", <= JSON
        qualifier: @qualifier, # "1",
      }

      begin
        resp = lambda_client.invoke(options)
      rescue Aws::Lambda::Errors::ResourceNotFoundException
        puts "The function #{function_name} was not found.  Maybe check the spelling or the AWS_PROFILE?".color(:red)
        return
      end

      if @options[:show_logs] || @options[:show_log]
        puts "Last 4KB of log in the x-amz-log-result header:".color(:green)
        puts Base64.decode64(resp.log_result)
      end

      add_console_link_to_clipboard
      result = resp.payload.read # already been normalized/JSON.dump by AWS
      unless @options[:mute_output]
        STDOUT.puts result # only thing that goes to stdout
      end
    end

    def guesser
      @guesser ||= Guesser.new(@provided_function_name)
    end

    def ensure_guesses_found!
      unless guesser.class_name and guesser.method_name
        puts guesser.error_message
        exit
      end
    end

    # @event is String because it can be the file:// notation
    # Returns text String for the lambda.invoke payload.
    def transformed_event
      text = @event
      if text && text.include?("file://")
        text = load_event_from_file(text)
      end

      check_valid_json!(text)

      puts "Function name: #{function_name.color(:green)}" unless @options[:mute]
      return text unless function_name.include?("_controller-")
      return text if @options[:lambda_proxy] == false

      event = JSON.load(text)
      lambda_proxy = {"queryStringParameters" => event}
      JSON.dump(lambda_proxy)
    end

    def load_event_from_file(text)
      path = text.gsub('file://','')
      path = "#{Jets.root}/#{path}" unless path[0..0] == '/'
      unless File.exist?(path)
        puts "File #{path} does not exist.  Are you sure the file exists?".color(:red)
        exit
      end
      text = IO.read(path)
    end

    # Exits with friendly error message when user provides bad just
    def check_valid_json!(text)
      JSON.load(text)
    rescue JSON::ParserError => e
      puts "Invalid json provided:\n  '#{text}'"
      puts "Exiting... Please try again and provide valid json."
      exit 1
    end

    # So use can quickly paste this into their browser if they want to see the function
    # via the Lambda console
    def add_console_link_to_clipboard
      return unless RUBY_PLATFORM =~ /darwin/
      return unless system("type pbcopy > /dev/null")

      # TODO: for add_console_link_to_clipboard get the region from the ~/.aws/config and AWS_PROFILE setting
      region = Aws::S3::Client.new.config.region || ENV["AWS_REGION"] ||'us-east-1'
      link = "https://console.aws.amazon.com/lambda/home?region=#{region}#/functions/#{function_name}?tab=configuration"
      system("echo #{link} | pbcopy")
      puts "Pro tip: The Lambda Console Link to the #{function_name} function has been added to your clipboard." unless @options[:mute]
    end

    # TODO: Hook client_context up and make sure it works. Think I've figure out how to sign client_context below.
    # Client context must be a valid Base64-encoded JSON object
    # Example: http://docs.aws.amazon.com/mobileanalytics/latest/ug/PutEvents.html
    def client_context
      context = {
        "client" => {
          "client_id" => "Jets",
          "app_title" => "jets call cli",
          "app_version_name" => Jets::VERSION,
        },
        "custom" => {},
        "env" =>{
          "platform" => RUBY_PLATFORM,
          "platform_version" => RUBY_VERSION,
        }
      }
      Base64.encode64(JSON.dump(context))
    end

    # For this class redirect puts to stderr so user can pipe output to tools like
    # jq. Example:
    #   jets call posts_controller-index '{"test":1}' | jq .
    def puts(text)
      $stderr.puts(text)
    end

    def lambda_client
      opt = {}
      opt = opt.merge({retry_limit: @options[:retry_limit]}) if @options[:retry_limit].present?
      opt = opt.merge({http_read_timeout: @options[:read_timeout]}) if @options[:read_timeout].present?

      if opt.empty?
        aws_lambda
      else
        Aws::Lambda::Client.new(opt)
      end
    end
  end
end