# frozen_string_literal: true

## --- BEGIN LICENSE BLOCK ---
# Original work Copyright (c) 2015-present the fastlane authors
# Modified work Copyright 2016-present WeWantToKnow AS
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
## --- END LICENSE BLOCK ---

require 'u3d_core/credentials'

module U3dCore
  # Executes commands and takes care of error handling and more
  class CommandExecutor
    class << self
      # Cross-platform way of finding an executable in the $PATH. Respects the $PATHEXT, which lists
      # valid file extensions for executables on Windows.
      #
      #    which('ruby') #=> /usr/bin/ruby
      #
      # Derived from http://stackoverflow.com/a/5471032/3005
      def which(cmd)
        # PATHEXT contains the list of file extensions that Windows considers executable, semicolon separated.
        # e.g. ".COM;.EXE;.BAT;.CMD"
        exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']

        ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
          exts.each do |ext|
            cmd_path = File.expand_path("#{cmd}#{ext}", path)
            return cmd_path if File.executable?(cmd_path) && !File.directory?(cmd_path)
          end
        end

        return nil
      end

      # @param command [String] The command to be executed
      # @param print_all [Boolean] Do we want to print out the command output while running?
      # @param print_command [Boolean] Should we print the command that's being executed
      # @param error [Block] A block that's called if an error occurs
      # @param prefix [Array] An array containg a prefix + block which might get applied to the output
      # @param admin [Boolean] Do we need admin privilege for this command?
      # @return [String] All the output as string
      # @deprecated
      def execute(command: nil, print_all: false, print_command: true, error: nil, prefix: nil, admin: false)
        print_all = true if U3dCore::Globals.verbose?
        prefix ||= {}

        output_callback = nil
        if print_all
          output_callback = proc do |line|
            # Prefix the current line with a string
            prefix.each do |element|
              line = element[:prefix] + line if element[:block]&.call(line)
            end
            UI.command_output(line)
          end
        end

        execute_command(command: command, output_callback: output_callback, print_command: print_command, error_callback: error, admin: admin)
      end

      # @param command [String] The command to be executed
      # @param output_callback [Block] the command to pass to print output
      # @param print_command [Boolean] Should we print the command that's being executed
      # @param error_callback [Block] A block that's called if an error occurs
      # @param admin [Boolean] Do we need admin privilege for this command?
      # @return [String] All the output as string
      def execute_command(command: nil, output_callback: nil, print_command: true, error_callback: nil, admin: false)
        command = command.join(' ') if command.is_a?(Array)
        UI.command(command) if print_command

        command = grant_admin_privileges(command) if admin

        execute_command_low(command: command, output_callback: output_callback, error_callback: error_callback)
      end

      def execute_command_low(command: nil, output_callback: nil, error_callback: nil)
        output = []
        begin
          status = U3dCore::Runner.run(command) do |stdin, _stdout, _pid|
            stdin.each do |l|
              line = l.strip # strip so that \n gets removed
              output << line

              output_callback&.call(l)
            end
          end
          raise "Exit status: #{status}".red if !status.nil? && status.nonzero?
        rescue StandardError => e
          # This could happen
          # * if the status is failed
          # * when the environment is wrong:
          # > invalid byte sequence in US-ASCII (ArgumentError)
          output << e.to_s
          o = output.join("\n")
          UI.verbose o
          raise e unless error_callback

          error_callback.call(o, nil)
        end
        return output.join("\n")
      end

      # rubocop:disable Naming/PredicateName
      def has_admin_privileges?(retry_count: 2)
        # rubocop:enable Naming/PredicateName
        if Helper.windows?
          begin
            result = system_no_output('reg query HKU\\S-1-5-19')
          rescue StandardError
            result = false
          end
        else
          env_username = ENV['USER']
          if env_username == "root"
            result = true
          else
            credentials = U3dCore::Credentials.new(user: env_username)
            begin
              result = system_no_output("sudo -k && echo #{credentials.password.shellescape} | sudo -S /usr/bin/whoami")
            rescue StandardError
              result = false
            end
            credentials.forget_credentials unless result # FIXME: why?
          end
        end
        # returns false if result is nil (command execution fail)
        result = result ? true : false
        result = has_admin_privileges?(retry_count: retry_count - 1) unless retry_count <= 0 || result
        result
      end

      def grant_admin_privileges(command)
        if Helper.windows?
          raise CredentialsError, "The command '#{command}' must be run in administrative shell" unless has_admin_privileges?
        else
          env_username = ENV['USER']
          unless env_username == "root"
            cred = U3dCore::Credentials.new(user: env_username)
            raise CredentialsError, "The command '#{command}' must be run with admin privileges" unless has_admin_privileges?

            command = "sudo -k && echo #{cred.password.shellescape} | sudo -S bash -c \"#{command}\""
          end
        end
        UI.verbose 'Admin privileges granted for command execution'
        command
      end

      def system_no_output(command)
        system(command, out: File::NULL, err: File::NULL)
      end
    end

    private_class_method :execute_command_low, :system_no_output
  end
end
