module EbDeployer
  class ResourceNotInReadyState < StandardError
  end

  class CloudFormationProvisioner
    SUCCESS_STATS = [:create_complete, :update_complete, :update_rollback_complete]
    FAILED_STATS = [:create_failed, :update_failed]

    def initialize(stack_name, cf_driver)
      @stack_name = stack_name
      @cf_driver = cf_driver
    end

    def provision(resources)
      resources = symbolize_keys(resources)
      template = File.read(resources[:template])
      capabilities = resources[:capabilities] || []
      params = resources[:inputs] || resources[:parameters] || {}
      stack_exists? ? update_stack(template, params, capabilities) : create_stack(template, params, capabilities)
      wait_for_stack_op_terminate
    end

    def transform_outputs(resources)
      resources = symbolize_keys(resources)
      outputs = resources[:outputs] || {}
      transforms = resources[:transforms] || {}
      transform_output_to_settings(convert_to_transforms(outputs).merge(transforms))
    end

    def output(key)
      @cf_driver.query_output(@stack_name, key)
    rescue Aws::CloudFormation::Errors::ValidationError
      raise ResourceNotInReadyState.new("Resource stack not in ready state yet, perhaps you should provision it first?")
    end

    private

    #todo: remove duplication
    def symbolize_keys(hash)
      hash.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
    end


    def convert_to_transforms(outputs)
      outputs.inject({}) do |memo, (key, value)|
        memo[key] = lambda { |output_value| value.merge('value' => output_value) }
        memo
      end
    end

    def update_stack(template, params, capabilities)
      @cf_driver.update_stack(@stack_name, template,
                              :capabilities => capabilities,
                              :parameters => params)
    end

    def stack_exists?
      @cf_driver.stack_exists?(@stack_name)
    end

    def create_stack(template, params, capabilities)
      @cf_driver.create_stack(@stack_name, template,
                              :disable_rollback => true,
                              :capabilities => capabilities,
                              :parameters => params)
    end

    def stack_status
      @cf_driver.stack_status(@stack_name)
    end

    def transform_output_to_settings(transforms)
      (transforms || []).inject([]) do |settings, pair|
        key, transform = pair
        settings << transform.call(output(key))
        settings
      end.flatten
    end

    def wait_for_stack_op_terminate
      stats = stack_status
      while !SUCCESS_STATS.include?(stats)
        sleep 15
        stats = stack_status
        raise "Resource stack update failed!" if FAILED_STATS.include?(stats)
        log "current status: #{stack_status}"
      end
    end

    def log(msg)
      puts "[#{Time.now.utc}][cloud_formation_provisioner] #{msg}"
    end
  end
end
