require 'csv'
require 'puppet'

class Puppet::Error::OpenstackAuthInputError < Puppet::Error
end

class Puppet::Error::OpenstackUnauthorizedError < Puppet::Error
end

class Puppet::Provider::Openstack < Puppet::Provider

  initvars # so commands will work
  commands :openstack => 'openstack'

  def request(service, action, object, credentials, *properties)
    if password_credentials_set?(credentials)
      auth_args = password_auth_args(credentials)
    elsif openrc_set?(credentials)
      credentials = get_credentials_from_openrc(credentials['openrc'])
      auth_args = password_auth_args(credentials)
    elsif service_credentials_set?(credentials)
      auth_args = token_auth_args(credentials)
    elsif env_vars_set?
      # noop; auth needs no extra arguments
      auth_args = nil
    else  # All authentication efforts failed
      raise(Puppet::Error::OpenstackAuthInputError, 'No credentials provided.')
    end
    args = [object, properties, auth_args].flatten.compact
    authenticate_request(service, action, args)
  end

  def self.request(service, action, object, *properties)
    if env_vars_set?
      # noop; auth needs no extra arguments
      auth_args = nil
    else  # All authentication efforts failed
      raise(Puppet::Error::OpenstackAuthInputError, 'No credentials provided.')
    end
    args = [object, properties, auth_args].flatten.compact
    authenticate_request(service, action, args)
  end

  # Returns an array of hashes, where the keys are the downcased CSV headers
  # with underscores instead of spaces
  def self.authenticate_request(service, action, *args)
    rv = nil
    timeout = 10
    end_time = Time.now.to_i + timeout
    loop do
      begin
        if(action == 'list')
          response = openstack(service, action, '--quiet', '--format', 'csv', args)
          response = parse_csv(response)
          keys = response.delete_at(0) # ID,Name,Description,Enabled
          rv = response.collect do |line|
            hash = {}
            keys.each_index do |index|
              key = keys[index].downcase.gsub(/ /, '_').to_sym
              hash[key] = line[index]
            end
            hash
          end
        elsif(action == 'show' || action == 'create')
          rv = {}
          # shell output is name="value"\nid="value2"\ndescription="value3" etc.
          openstack(service, action, '--format', 'shell', args).split("\n").each do |line|
            # key is everything before the first "="
            key, val = line.split("=", 2)
            next unless val # Ignore warnings
            # value is everything after the first "=", with leading and trailing double quotes stripped
            val = val.gsub(/\A"|"\Z/, '')
            rv[key.downcase.to_sym] = val
          end
        else
          rv = openstack(service, action, args)
        end
        break
      rescue Puppet::ExecutionFailure => e
        if e.message =~ /HTTP 401/
          raise(Puppet::Error::OpenstackUnauthorizedError, 'Could not authenticate.')
        elsif e.message =~ /Unable to establish connection/
          current_time = Time.now.to_i
          if current_time > end_time
            break
          else
            wait = end_time - current_time
            Puppet::debug("Non-fatal error: \"#{e.message}\"; retrying for #{wait} more seconds.")
            if wait > timeout - 2 # Only notice the first time
              notice("#{service} service is unavailable. Will retry for up to #{wait} seconds.")
            end
          end
          sleep(2)
        else
          raise e
        end
      end
    end
    return rv
  end

  def authenticate_request(service, action, *args)
    self.class.authenticate_request(service, action, *args)
  end

  private

  def password_credentials_set?(auth_params)
    auth_params && auth_params['username'] && auth_params['password'] && auth_params['project_name'] && auth_params['auth_url']
  end

  def openrc_set?(auth_params)
    auth_params && auth_params['openrc']
  end

  def service_credentials_set?(auth_params)
    auth_params && auth_params['token'] && auth_params['url']
  end

  def self.env_vars_set?
    ENV['OS_USERNAME'] && ENV['OS_PASSWORD'] && ENV['OS_PROJECT_NAME'] && ENV['OS_AUTH_URL']
  end

  def env_vars_set?
    self.class.env_vars_set?
  end

  def self.password_auth_args(credentials)
    creds = [ '--os-username',     credentials['username'],
              '--os-password',     credentials['password'],
              '--os-project-name', credentials['project_name'],
              '--os-auth-url',     credentials['auth_url'] ]

    if credentials.include?('project_domain_name')
      creds << '--os-project-domain-name'
      creds << credentials['project_domain_name']
    end

    if credentials.include?('user_domain_name')
      creds << '--os-user-domain-name'
      creds << credentials['user_domain_name']
    end

    creds
  end

  def password_auth_args(credentials)
    self.class.password_auth_args(credentials)
  end

  def self.token_auth_args(credentials)
    [ '--os-token', credentials['token'],
      '--os-url',   credentials['url'] ]
  end

  def token_auth_args(credentials)
    self.class.token_auth_args(credentials)
  end

  def get_credentials_from_openrc(file)
    creds = {}
    File.open(file).readlines.delete_if{|l| l=~ /^#/}.each do |line|
      key, value = line.split('=')
      key = key.split(' ').last.downcase.sub(/^os_/, '')
      value = value.chomp.gsub(/'/, '')
      creds[key] = value
    end
    return creds
  end

  def self.get_credentials_from_env
    env = ENV.to_hash.dup.delete_if { |key, _| ! (key =~ /^OS_/) }
    credentials = {}
    env.each do |name, value|
      credentials[name.downcase.sub(/^os_/, '')] = value
    end
    credentials
  end

  def get_credentials_from_env
    self.class.get_credentials_from_env
  end

  def self.parse_csv(text)
    # Ignore warnings - assume legitimate output starts with a double quoted
    # string.  Errors will be caught and raised prior to this
    text = text.split("\n").drop_while { |line| line !~ /^\".*\"/ }.join("\n")
    return CSV.parse(text + "\n")
  end

end
