# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1

require 'digest/sha1'

module VagrantPlugins
  module ContainerProvisioner
    class Client
      def initialize(machine, container_command)
        @machine = machine
        @container_command =  container_command
      end

      # Build an image given a path to a Dockerfile
      #
      # @param [Array<Array<String, Hash>>] - Path and options to the Dockerfile to pass to
      #   container build command
      def build_images(images)
        @machine.communicate.tap do |comm|
          images.each do |path, opts|
            @machine.ui.info(I18n.t("vagrant.container_building_single", path: path))
            comm.sudo("#{@container_command} build #{opts[:args]} #{path}") do |type, data|
              handle_comm(type, data)
            end
          end
        end
      end

      # Pull image given a list of images
      #
      # @param [String] - Image name
      def pull_images(*images)
        @machine.communicate.tap do |comm|
          images.each do |image|
            @machine.ui.info(I18n.t("vagrant.container_pulling_single", name: image))
            comm.sudo("#{@container_command} pull #{image}") do |type, data|
              handle_comm(type, data)
            end
          end
        end
      end

      def run(containers)
        containers.each do |name, config|
          cids_dir = "/var/lib/vagrant/cids"
          config[:cidfile] ||= "#{cids_dir}/#{Digest::SHA1.hexdigest name}"

          @machine.ui.info(I18n.t("vagrant.container_running", name: name))
          @machine.communicate.sudo("mkdir -p #{cids_dir}")
          run_container({
            name: name,
            original_name: name,
          }.merge(config))
        end
      end

      # Run a OCI container. If the container does not exist it will be
      # created. If the image is stale it will be recreated and restarted
      def run_container(config)
        raise "Container's cidfile was not provided!" if !config[:cidfile]

        id = "$(cat #{config[:cidfile]})"

        if container_exists?(id)
          if container_args_changed?(config)
            @machine.ui.info(I18n.t("vagrant.container_restarting_container_args",
              name: config[:name],
            ))
            stop_container(id)
            create_container(config)
          elsif container_image_changed?(config)
            @machine.ui.info(I18n.t("vagrant.container_restarting_container_image",
              name: config[:name],
            ))
            stop_container(id)
            create_container(config)
          else
            start_container(id)
          end
        else
          create_container(config)
        end
      end

      def container_exists?(id)
        lookup_container(id, true)
      end

      # Start container
      #
      # @param String - Image id
      def start_container(id)
        if !container_running?(id)
          @machine.communicate.sudo("#{@container_command} start #{id}")
        end
      end

      # Stop and remove container
      #
      # @param String - Image id
      def stop_container(id)
        @machine.communicate.sudo %[
          #{@container_command} stop #{id}
          #{@container_command} rm #{id}
        ]
      end

      def container_running?(id)
        lookup_container(id)
      end

      def container_image_changed?(config)
        # Returns true if there is a container running with the given :name,
        # and the container is not using the latest :image.

        # Here, "<cmd> inspect <container>" returns the id of the image
        # that the container is using. We check that the latest image that
        # has been built with that name (:image)  matches the one that the
        # container is running.
        cmd = ("#{@container_command} inspect --format='{{.Image}}' #{config[:name]} |" +
               " grep $(#{@container_command} images -q #{config[:image]})")
        return !@machine.communicate.test(cmd)
      end

      def container_args_changed?(config)
        path = container_data_path(config)
        return true if !path.exist?

        args = container_run_args(config)
        sha  = Digest::SHA1.hexdigest(args)
        return true if path.read.chomp != sha

        return false
      end

      def create_container(config)
        args = container_run_args(config)

        @machine.communicate.sudo %[rm -f "#{config[:cidfile]}"]
        @machine.communicate.sudo %[#{@container_command} run #{args}]

        sha  = Digest::SHA1.hexdigest(args)
        container_data_path(config).open("w+") do |f|
          f.write(sha)
        end
      end

      # Looks up if a container with a given id exists using the
      # `ps` command. Returns Boolean
      #
      # @param String - Image id
      def lookup_container(id, list_all = false)
        container_ps = "sudo #{@container_command} ps -q"
        container_ps << " -a" if list_all
        @machine.communicate.tap do |comm|
          return comm.test("#{container_ps} --no-trunc | grep -wFq #{id}")
        end
      end

      def container_name(config)
        name = config[:name]

        # If the name is the automatically assigned name, then
        # replace the "/" with "-" because "/" is not a valid
        # character for a container name.
        name = name.gsub("/", "-").gsub(":", "-") if name == config[:original_name]
        name
      end

      # Compiles run arguments to be appended to command string.
      # Returns String
      def container_run_args(config)
        name = container_name(config)

        args = "--cidfile=#{config[:cidfile]} "
        args << "-d " if config[:daemonize]
        args << "--name #{name} " if name && config[:auto_assign_name]
        args << "--restart=#{config[:restart]}" if config[:restart]
        args << " #{config[:args]}" if config[:args]

        "#{args} #{config[:image]} #{config[:cmd]}".strip
      end

      def container_data_path(config)
        name = container_name(config)
        @machine.data_dir.join("#{@container_command}-#{name}")
      end

      protected

      # This handles outputting the communication data back to the UI
      def handle_comm(type, data)
        if [:stderr, :stdout].include?(type)
          # Clear out the newline since we add one
          data = data.chomp
          return if data.empty?

          options = {}
          #options[:color] = color if !config.keep_color

          @machine.ui.info(data.chomp, **options)
        end
      end
    end
  end
end
