# Copyright 2016 Matt Wrock <matt@mattwrock.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require "base64" unless defined?(Base64)
require_relative "message"
require_relative "message_data/pipeline_state"

module WinRM
  module PSRP
    # Handles decoding a raw powershell output response
    class PowershellOutputDecoder
      # rubocop:disable Metrics/CyclomaticComplexity
      # Decode the raw SOAP output into decoded PSRP message,
      # Removes BOM and replaces encoded line endings
      # @param raw_output [String] The raw encoded output
      # @return [String] The decoded output
      def decode(message)
        case message.type
        when WinRM::PSRP::Message::MESSAGE_TYPES[:pipeline_output]
          decode_pipeline_output(message)
        when WinRM::PSRP::Message::MESSAGE_TYPES[:runspacepool_host_call]
          decode_host_call(message)
        when WinRM::PSRP::Message::MESSAGE_TYPES[:pipeline_host_call]
          decode_host_call(message)
        when WinRM::PSRP::Message::MESSAGE_TYPES[:error_record]
          decode_error_record(message)
        when WinRM::PSRP::Message::MESSAGE_TYPES[:pipeline_state]
          if message.parsed_data.pipeline_state == WinRM::PSRP::MessageData::PipelineState::FAILED
            decode_error_record(message)
          end
        end
      end
      # rubocop:enable Metrics/CyclomaticComplexity

      protected

      def decode_pipeline_output(message)
        message.parsed_data.output
      end

      def decode_host_call(message)
        text = case message.parsed_data.method_identifier
          when /WriteLine/, "WriteErrorLine"
            "#{message.parsed_data.method_parameters[:s]}\r\n"
          when "WriteDebugLine"
            "Debug: #{message.parsed_data.method_parameters[:s]}\r\n"
          when "WriteWarningLine"
            "Warning: #{message.parsed_data.method_parameters[:s]}\r\n"
          when "WriteVerboseLine"
            "Verbose: #{message.parsed_data.method_parameters[:s]}\r\n"
          when /Write[1-2]/
            message.parsed_data.method_parameters[:s]
               end

        hex_decode(text)
      end

      def decode_error_record(message)
        parsed = message.parsed_data
        text = if message.type == WinRM::PSRP::Message::MESSAGE_TYPES[:pipeline_state]
                 render_exception_as_error_record(parsed.exception_as_error_record)
               else
                 case parsed.fully_qualified_error_id
                 when "Microsoft.PowerShell.Commands.WriteErrorException"
                   render_write_error_exception(parsed)
                 when "NativeCommandError"
                   render_native_command_error(parsed)
                 when "NativeCommandErrorMessage"
                   parsed.exception[:message]
                 else
                   render_exception(parsed)
                 end
               end

        hex_decode(text)
      end

      def render_write_error_exception(parsed)
        <<EOH
#{parsed.invocation_info[:line]} : #{parsed.exception[:message]}
    + CategoryInfo          : #{parsed.error_category_message}
    + FullyQualifiedErrorId : #{parsed.fully_qualified_error_id}
EOH
      end

      def render_exception(parsed)
        <<EOH
#{parsed.exception[:message]}
#{parsed.invocation_info[:position_message]}
    + CategoryInfo          : #{parsed.error_category_message}
    + FullyQualifiedErrorId : #{parsed.fully_qualified_error_id}
EOH
      end

      def render_native_command_error(parsed)
        <<EOH
#{parsed.invocation_info[:my_command]} : #{parsed.exception[:message]}
    + CategoryInfo          : #{parsed.error_category_message}
    + FullyQualifiedErrorId : #{parsed.fully_qualified_error_id}
EOH
      end

      def render_exception_as_error_record(parsed)
        <<EOH
#{parsed.exception[:message]}
    + CategoryInfo          : #{parsed.error_category_message}
    + FullyQualifiedErrorId : #{parsed.fully_qualified_error_id}
EOH
      end

      private

      def hex_decode(text)
        return unless text

        text.gsub(/_x(\h\h\h\h)_/) do
          decoded_text = Regexp.last_match[1].hex.chr.force_encoding("utf-8")
          if decoded_text.respond_to?(:scrub)
            decoded_text.scrub
          else
            decoded_text.encode("utf-16", invalid: :replace, undef: :replace).encode("utf-8")
          end
        end
      end
    end
  end
end
