module Backup
  module Packager
    class Error < Backup::Error; end

    class << self
      include Utilities::Helpers

      ##
      # Build the final package for the backup model.
      def package!(model)
        @package   = model.package
        @encryptor = model.encryptor
        @splitter  = model.splitter
        @pipeline  = Pipeline.new

        Logger.info "Packaging the backup files..."
        procedure.call

        if @pipeline.success?
          Logger.info "Packaging Complete!"
        else
          raise Error, "Failed to Create Backup Package\n" +
            @pipeline.error_messages
        end
      end

      private

      ##
      # Builds a chain of nested Procs which adds each command to a Pipeline
      # needed to package the final command to package the backup.
      # This is done so that the Encryptor and Splitter have the ability
      # to perform actions before and after the final command is executed.
      # No Encryptors currently utilize this, however the Splitter does.
      def procedure
        stack = []

        ##
        # Initial `tar` command to package the temporary backup folder.
        # The command's output will then be either piped to the Encryptor
        # or the Splitter (if no Encryptor), or through `cat` into the final
        # output file if neither are configured.
        @pipeline.add(
          "#{utility(:tar)} -cf - " \
          "-C '#{Config.tmp_path}' '#{@package.trigger}'",
          tar_success_codes
        )

        ##
        # If an Encryptor was configured, it will be called first
        # to add the encryption utility command to be piped through,
        # and amend the final package extension.
        # It's output will then be either piped into a Splitter,
        # or through `cat` into the final output file.
        if @encryptor
          stack << lambda do
            @encryptor.encrypt_with do |command, ext|
              @pipeline << command
              @package.extension << ext
              stack.shift.call
            end
          end
        end

        ##
        # If a Splitter was configured, the `split` utility command will be
        # added to the Pipeline to split the final output into multiple files.
        # Once the Proc executing the Pipeline has completed and returns back
        # to the Splitter, it will check the final output files to determine
        # if the backup was indeed split.
        # If so, it will set the package's chunk_suffixes. If not, it will
        # remove the '-aa' suffix from the only file created by `split`.
        #
        # If no Splitter was configured, the final file output will be
        # piped through `cat` into the final output file.
        stack <<
          if @splitter
            lambda do
              @splitter.split_with do |command|
                @pipeline << command
                stack.shift.call
              end
            end
          else
            lambda do
              outfile = File.join(Config.tmp_path, @package.basename)
              @pipeline << "#{utility(:cat)} > #{outfile}"
              stack.shift.call
            end
          end

        ##
        # Last Proc to be called runs the Pipeline the procedure built.
        # Once complete, the call stack will unwind back through the
        # preceeding Procs in the stack (if any)
        stack << -> { @pipeline.run }

        stack.shift
      end

      def tar_success_codes
        gnu_tar? ? [0, 1] : [0]
      end
    end
  end
end
