require 'fileutils'

module Beaker
  module DSL
    module InstallUtils
      # This module contains methods to assist in installing projects from source
      # that use ezbake for packaging.
      #
      module EZBakeUtils
        # @!group Public DSL Methods

        # Installs leiningen project with given name and version on remote host.
        #
        # @param [Host] host A single remote host on which to install the
        #   specified leiningen project.
        def install_from_ezbake(host)
          ezbake_validate_support host
          ezbake_tools_available?
          install_ezbake_tarball_on_host host
          ezbake_installsh host, 'service'
        end

        # Installs termini with given name and version on remote host.
        #
        # @param [Host] host A single remote host on which to install the
        #   specified leiningen project.
        def install_termini_from_ezbake(host)
          ezbake_validate_support host
          ezbake_tools_available?
          install_ezbake_tarball_on_host host
          ezbake_installsh host, 'termini'
        end

        # Install a development version of ezbake into the local m2 repository
        #
        # This can be useful if you want to work on a development branch of
        # ezbake that hasn't been released yet. Ensure your project dependencies
        # in your development branch include a reference to the -SNAPSHOT
        # version of the project for it to successfully pickup a pre-shipped
        # version of ezbake.
        #
        # @param url [String] git url
        # @param branch [String] git branch
        def ezbake_dev_build(url = 'git@github.com:puppetlabs/ezbake.git',
                             branch = 'master')
          ezbake_dir = 'tmp/ezbake'
          conditionally_clone url, ezbake_dir, branch
          lp = ezbake_lein_prefix

          Dir.chdir(ezbake_dir) do
            ezbake_local_cmd "#{lp} install",
                             throw_on_failure: true
          end
        end

        # @!endgroup

        class << self
          attr_accessor :config
        end

        # @!group Private helpers

        # Test for support in one place
        #
        # @param [Host] host host to check for support
        # @raise [RuntimeError] if OS is not supported
        # @api private
        def ezbake_validate_support(host)
          variant, version, = host['platform'].to_array
          return if variant =~ /^(fedora|el|redhat|centos|debian|ubuntu)$/

          raise "No support for #{variant} within ezbake_utils ..."
        end

        # Build, copy & unpack tarball on remote host
        #
        # @param [Host] host installation destination
        # @api private
        def install_ezbake_tarball_on_host(host)
          ezbake_stage unless ezbake_config

          # Skip installation if the remote directory exists
          result = on host, "test -d #{ezbake_install_dir}", acceptable_exit_codes: [0, 1]
          return if result.exit_code == 0

          ezbake_staging_dir = File.join('target', 'staging')
          Dir.chdir(ezbake_staging_dir) do
            ezbake_local_cmd 'rake package:tar'
          end

          local_tarball = ezbake_staging_dir + '/pkg/' + ezbake_install_name + '.tar.gz'
          remote_tarball = ezbake_install_dir + '.tar.gz'
          scp_to host, local_tarball, remote_tarball

          # untar tarball on host
          on host, 'tar -xzf ' + remote_tarball

          # Check to ensure directory exists
          on host, "test -d #{ezbake_install_dir}"
        end

        LOCAL_COMMANDS_REQUIRED = [
          ['leiningen', 'lein --version', nil],
          ['lein-pprint', 'lein with-profile ci pprint :version',
           'Must have lein-pprint installed under the :ci profile.',],
          ['java', 'java -version', nil],
          ['git', 'git --version', nil],
          ['rake', 'rake --version', nil],
        ]

        # Checks given host for the tools necessary to perform
        # install_from_ezbake.
        #
        # @raise [RuntimeError] if tool is not found
        # @api private
        def ezbake_tools_available?
          LOCAL_COMMANDS_REQUIRED.each do |software_name, command, additional_error_message|
            next if system command

            error_message = "Must have #{software_name} installed on development system.\n"
            error_message += additional_error_message if additional_error_message
            raise error_message
          end
        end

        # Return the ezbake config.
        #
        # @return [Hash] configuration for ezbake, usually from ezbake.rb
        # @api private
        def ezbake_config
          EZBakeUtils.config
        end

        # Returns a leiningen prefix with local m2 repo capability
        #
        # @return [String] lein prefix command that uses a local build
        #   m2 repository.
        # @api private
        def ezbake_lein_prefix
          # Get the absolute path to the local repo
          m2_repo = File.join(Dir.pwd, 'tmp', 'm2-local')

          'lein update-in : assoc :local-repo "\"' + m2_repo + '\"" --'
        end

        # Prepares a staging directory for the specified project.
        #
        # @api private
        def ezbake_stage
          # Install the PuppetDB jar into the local repository
          ezbake_local_cmd "#{ezbake_lein_prefix} install",
                           throw_on_failure: true

          # Run ezbake stage
          ezbake_local_cmd "#{ezbake_lein_prefix} with-profile ezbake ezbake stage",
                           throw_on_failure: true

          # Boostrap packaging, and grab configuration info from project
          staging_dir = File.join('target', 'staging')
          Dir.chdir(staging_dir) do
            ezbake_local_cmd 'rake package:bootstrap'

            load 'ezbake.rb'
            ezbake = EZBake::Config
            ezbake[:package_version] = `printf $(rake pl:print_build_param[ref] | tail -n 1)`
            EZBakeUtils.config = ezbake
          end
        end

        # Executes a local command using system, logging the prepared command
        #
        # @param [String] cmd command to execute
        # @param [Hash] opts options
        # @option opts [bool] :throw_on_failure If true, throws an
        #   exception if the exit code is non-zero. Defaults to false.
        # @return [bool] true if exit == 0 false if otherwise
        # @raise [RuntimeError] if :throw_on_failure is true and
        #   command fails
        # @api private
        def ezbake_local_cmd(cmd, opts = {})
          opts = {
            throw_on_failure: false,
          }.merge(opts)

          logger.notify "localhost $ #{cmd}"
          result = system cmd
          raise "Command failure #{cmd}" if opts[:throw_on_failure] && result == false

          result
        end

        # Retrieve the tarball installation name. This is the name of
        # the tarball without the .tar.gz extension, and the name of the
        # path where it will unpack to.
        #
        # @return [String] name of the tarball and directory
        # @api private
        def ezbake_install_name
          ezbake = ezbake_config
          project_package_version = ezbake[:package_version]
          project_name = ezbake[:project]
          format('%s-%s', project_name, project_package_version)
        end

        # Returns the full path to the installed software on the remote host.
        #
        # This only returns the path, it doesn't work out if its installed or
        # not.
        #
        # @return [String] path to the installation dir
        # @api private
        def ezbake_install_dir
          "/root/#{ezbake_install_name}"
        end

        # A helper that wraps the execution of install.sh in the proper
        # ezbake installation directory.
        #
        # @param [Host] host Host to run install.sh on
        # @param [String] task Task to execute with install.sh
        # @api private
        def ezbake_installsh(host, task = '')
          on host, "cd #{ezbake_install_dir}; bash install.sh #{task}"
        end

        # Only clone from given git URI if there is no existing git clone at the
        # given local_path location.
        #
        # @param [String] upstream_uri git URI
        # @param [String] local_path path to conditionally install to
        # @param [String] branch to checkout
        # @api private
        def conditionally_clone(upstream_uri, local_path, branch = 'origin/HEAD')
          if ezbake_local_cmd "git --work-tree=#{local_path} --git-dir=#{local_path}/.git status"
            ezbake_local_cmd "git --work-tree=#{local_path} --git-dir=#{local_path}/.git fetch origin"
            ezbake_local_cmd "git --work-tree=#{local_path} --git-dir=#{local_path}/.git checkout #{branch}"
          else
            parent_dir = File.dirname(local_path)
            FileUtils.mkdir_p(parent_dir)
            ezbake_local_cmd "git clone #{upstream_uri} #{local_path}"
            ezbake_local_cmd "git --work-tree=#{local_path} --git-dir=#{local_path}/.git checkout #{branch}"
          end
        end
      end
    end
  end
end