module Ridley
  class Sandbox
    class << self
      # @param [Ridley::Connection] connection
      # @param [Array] checksums
      #
      # @return [Ridley::Sandbox]
      def create(connection, checksums = [])
        sumhash = { checksums: Hash.new }.tap do |chks|
          Array(checksums).each { |chk| chks[:checksums][chk] = nil }
        end

        attrs = connection.post("sandboxes", sumhash.to_json).body
        new(connection, attrs[:sandbox_id], attrs[:checksums])
      end

      # Checksum the file at the given filepath for a Chef API.
      #
      # @param [String] path
      #
      # @return [String]
      def checksum(path)
        File.open(path, 'rb') { |f| checksum_io(f, Digest::MD5.new) }
      end

      # Checksum and encode the file at the given filepath for uploading
      #
      # @param [String] path
      #
      # @return [String]
      #   a base64 encoded checksum
      def checksum64(path)
        Base64.encode64([checksum(path)].pack("H*")).strip
      end

      # @param [String] io
      # @param [Object] digest
      #
      # @return [String]
      def checksum_io(io, digest)
        while chunk = io.read(1024 * 8)
          digest.update(chunk)
        end
        digest.hexdigest
      end
    end

    attr_reader :sandbox_id
    attr_reader :checksums

    def initialize(connection, id, checksums)
      @connection = connection
      @sandbox_id = id
      @checksums = checksums
    end

    def checksum(chk_id)
      checksums.fetch(chk_id.to_sym)
    end

    # @param [String] chk_id
    # @param [String] path
    #
    # @return [Hash, nil]
    def upload(chk_id, path)
      checksum = self.checksum(chk_id)
      
      unless checksum[:needs_upload]
        return nil
      end

      headers  = {
        'Content-Type' => 'application/x-binary',
        'content-md5' => self.class.checksum64(path)
      }
      contents = File.open(path, 'rb') { |f| f.read }

      # Hosted Chef returns Amazon S3 URLs for where to upload
      # checksums to. OSS Chef Server and Hosted Chef return URLs
      # to the same Chef API that the Sandbox creation request was
      # sent to.
      #
      # The connection object is duplicated to ensure all of our connection
      # settings persist, but the scheme, host, and port are set to the
      # value of the given checksum.
      conn = connection.send(:conn).dup

      url = URI(checksum[:url])
      upload_path = url.path
      url.path = ""

      conn.url_prefix = url.to_s

      conn.put(upload_path, contents, headers)
    end

    def commit
      connection.put("sandboxes/#{sandbox_id}", { is_completed: true }.to_json).body
    end

    def to_s
      "#{sandbox_id}: #{checksums}"
    end

    private

      attr_reader :connection
  end

  module DSL
    # Coerces instance functions into class functions on Ridley::Sandbox. This coercion
    # sends an instance of the including class along to the class function.
    #
    # @see Ridley::ChainLink
    #
    # @return [Ridley::ChainLink]
    #   a context object to delegate instance functions to class functions on Ridley::Sandbox
    def sandbox
      ChainLink.new(self, Ridley::Sandbox)
    end
  end
end