# frozen_string_literal: true

require 'logging'
require 'bolt/node/errors'

module Bolt
  module Transport
    class Docker < Base
      class Connection
        def initialize(target)
          # lazy-load expensive gem code
          require 'docker'

          @target = target
          @logger = Logging.logger[target.host]
        end

        def connect
          # Explicitly create the new Connection to avoid relying on global state in the Docker module.
          url = @target.options['service-url'] || ::Docker.url
          options = ::Docker.options.merge(@target.options['service-options'] || {})
          @container = ::Docker::Container.get(@target.host, {}, ::Docker::Connection.new(url, options))
          @logger.debug { "Opened session" }
        rescue StandardError => e
          raise Bolt::Node::ConnectError.new(
            "Failed to connect to #{@target.uri}: #{e.message}",
            'CONNECT_ERROR'
          )
        end

        def execute(*command, options)
          command.unshift(options[:interpreter]) if options[:interpreter]
          if options[:environment]
            envs = options[:environment].map { |env, val| "#{env}=#{val}" }
            command = ['env'] + envs + command
          end

          @logger.debug { "Executing: #{command}" }
          result = @container.exec(command, options) { |stream, chunk| @logger.debug("#{stream}: #{chunk}") }
          if result[2] == 0
            @logger.debug { "Command returned successfully" }
          else
            @logger.info { "Command failed with exit code #{result[2]}" }
          end
          result[0] = result[0].join.force_encoding('UTF-8')
          result[1] = result[1].join.force_encoding('UTF-8')
          result
        rescue StandardError
          @logger.debug { "Command aborted" }
          raise
        end

        def write_remote_file(source, destination)
          @container.store_file(destination, File.binread(source))
        rescue StandardError => e
          raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
        end

        def write_remote_directory(source, destination)
          tar = ::Docker::Util.create_dir_tar(source)
          mkdirs([destination])
          @container.archive_in_stream(destination) { tar.read(Excon.defaults[:chunk_size]).to_s }
        rescue StandardError => e
          raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
        end

        def mkdirs(dirs)
          _, stderr, exitcode = execute('mkdir', '-p', *dirs, {})
          if exitcode != 0
            message = "Could not create directories: #{stderr}"
            raise Bolt::Node::FileError.new(message, 'MKDIR_ERROR')
          end
        end

        def make_tempdir
          tmpdir = @target.options.fetch('tmpdir', '/tmp')
          tmppath = "#{tmpdir}/#{SecureRandom.uuid}"

          stdout, stderr, exitcode = execute('mkdir', '-m', '700', tmppath, {})
          if exitcode != 0
            raise Bolt::Node::FileError.new("Could not make tempdir: #{stderr}", 'TEMPDIR_ERROR')
          end
          tmppath || stdout.first
        end

        def with_remote_tempdir
          dir = make_tempdir
          yield dir
        ensure
          if dir
            _, stderr, exitcode = execute('rm', '-rf', dir, {})
            if exitcode != 0
              @logger.warn("Failed to clean up tempdir '#{dir}': #{stderr}")
            end
          end
        end

        def write_remote_executable(dir, file, filename = nil)
          filename ||= File.basename(file)
          remote_path = File.join(dir.to_s, filename)
          write_remote_file(file, remote_path)
          make_executable(remote_path)
          remote_path
        end

        def make_executable(path)
          _, stderr, exitcode = execute('chmod', 'u+x', path, {})
          if exitcode != 0
            message = "Could not make file '#{path}' executable: #{stderr}"
            raise Bolt::Node::FileError.new(message, 'CHMOD_ERROR')
          end
        end
      end
    end
  end
end