# Copyright 2020-present Facebook
#
# 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.

module TasteTester
  class SSH
    module Util
      def jumps
        TasteTester::Config.jumps ? "-J #{TasteTester::Config.jumps}" : ''
      end

      def ssh_cmd_generator
        if TasteTester::Config.ssh_cmd_gen_template
          base_gen_args = {
            :user => TasteTester::Config.user,
            :jumps => jumps,
            :host => @host,
          }
          TasteTester::Config.ssh_cmd_gen_template %
            base_gen_args
        end
      end

      def ssh_generated_cmd
        if TasteTester::Config.ssh_cmd_gen_template
          # we store this generated command inside a class variable
          # so that we can directly refer to this while printing
          # logs and error messages
          begin
            # run the generator command only if it's not run already
            unless @ssh_generated_cmd
              generator = Mixlib::ShellOut.new(ssh_cmd_generator).run_command
              generator.error!
              @ssh_generated_cmd = generator.stdout.chomp
            end
            "#{@ssh_generated_cmd} #{@tunnel_options}".strip
          rescue Mixlib::ShellOut::ShellCommandFailed => e
            logger.error("The generator command: #{ssh_cmd_generator} " +
                         'failed during execution')
            logger.error(e.message)
            exit(1)
          end
        end
      end

      def ssh_vanilla_cmd
        "#{TasteTester::Config.ssh_command} #{jumps} -T -o BatchMode=yes " +
          '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no ' +
          "-o ConnectTimeout=#{TasteTester::Config.ssh_connect_timeout} " +
          @extra_options.to_s +
          "#{TasteTester::Config.user}@#{@host}"
      end

      def ssh_base_cmd
        if TasteTester::Config.ssh_cmd_gen_template
          ssh_generated_cmd
        else
          ssh_vanilla_cmd
        end
      end

      def error!
        error = <<~ERRORMESSAGE
  SSH returned error while connecting to #{@host}
  The host might be broken or your SSH access is not working properly
  Try running the following command to see if ssh is good:

      #{ssh_base_cmd} -v

      ERRORMESSAGE

        if TasteTester::Config.ssh_cmd_gen_template
          error += <<~ERRORMESSAGE

  The above command was generated, and it may be useful to run the generator directly instead:

      #{ssh_cmd_generator}
      ERRORMESSAGE
        end

        error += <<~ERRORMESSAGE

  If ssh works, add '-v' key to taste-tester to see the list of commands it's
  trying to execute, and try to run them manually on destination host
        ERRORMESSAGE
        logger.error(error)
        fail TasteTester::Exceptions::SshError
      end

      def build_ssh_cmd(ssh, command_list)
        if TasteTester::Config.windows_target
          # Powershell has no `&&`. So originally we looked into joining the
          # various commands with `; if ($LASTEXITCODE -ne 0) { exit 42 }; `
          # except that it turns out lots of Powershell commands don't set
          # $LASTEXITCODE and so that crashes a lot.
          #
          # There is an `-and`, but it only works if you group things together
          # with `()`, but that loses any output.
          #
          # Technically in the latest preview of Powershell 7, `&&` exists, but
          # we cannot rely on this.
          #
          # So here we are. Thanks Windows Team.
          #
          # Anyway, what we *really* care about is that we exit if we_testing()
          # errors out, and on Windows, we can do that straight from the
          # powershell we generate there (we're not forking off awk), so the
          # `&&` isn't as critical. It's still a bummer that we continue on
          # if one of the commands fails, but... Well, it's Windows,
          # whatchyagonnado?

          cmds = command_list.join(' ; ')
        else
          cmds = command_list.join(' && ')
        end
        cmd = "#{ssh} "
        cc = Base64.encode64(cmds).delete("\n")
        if TasteTester::Config.windows_target

          # This is pretty horrible, but because there's no way I can find to
          # take base64 as stdin and output text, we end up having to do use
          # these PS functions. But they're going to pass through *both* bash
          # *and* powershell, so in order to preserve the quotes, it gets
          # pretty ugly.
          #
          # The tldr here is that in shell you can't escape quotes you're
          # using to quote something. So if you use single quotes, there's no
          # way to escape a single quote inside, and same with double-quotes.
          # As such we switch between quote-styles as necessary. As long as the
          # strings are back-to-back, shell handles this well. To make this
          # clear, imagine you want to echo this:
          #    '"'"
          # Exactly like that. You would quote the first single quotes in double
          # quotes: "'"
          # Then the double quotes in single quotes: '"'
          # Now repeat twice and you get: echo "'"'"'"'"'"'
          # And that works reliably.
          #
          # We're doing the same thing here. What we want on the other side of
          # the ssh is:
          #  [Text.Encoding]::Utf8.GetString([Convert]::FromBase64String('...'))
          #
          # But for this to work right the command we pass to SSH has to be in
          # single quotes too. For simplicity lets call those two functions
          # above GetString() and Base64(). So we'll start with:
          #    ssh host 'GetString(Base64('
          # We've closed that string, now we add the single quote we want there,
          # as well as the stuff inside of those double quotes, so we'll add:
          #    '#{cc}'))
          # but that must be in double quotes since we're using single quotes.
          # Put that together:
          #    ssh host 'GetString(Base64('"'#{cc}'))"
          #             ^-----------------^^---------^
          #              string 1           string2
          # No we're doing with needing single quotes inside of our string, go
          # back to using single-quotes so no variables get interpolated. We now
          # add: ' | powershell.exe -c -; exit $LASTEXITCODE'
          #   ssh host 'GetString(Base64('"'#{cc}'))"' | powershell.exe ...'
          #            ^-----------------^^---------^^---------------------^
          #
          # More than you ever wanted to know about shell. You're welcome.
          #
          # But now we have to put it inside of a ruby string, :)

          # just for readability, put these crazy function names inside of
          # variables
          fun1 = '[Text.Encoding]::Utf8.GetString'
          fun2 = '[Convert]::FromBase64String'
          cmd += "'#{fun1}(#{fun2}('\"'#{cc}'))\"' | "
          #       ^----------------^ ^----------^^---
          #        single-q           double-q    single-q
          #        string 1           string2     string3
          cmd += 'powershell.exe -c -; exit $LASTEXITCODE\''
          #       ----------------------------------------^
          #       continued string3
        else
          cmd += "\"echo '#{cc}' | base64 --decode"
          if TasteTester::Config.user != 'root'
            cmd += ' | sudo bash -x"'
          else
            cmd += ' | bash -x"'
          end
        end
        cmd
      end
    end
  end
end
