class Gratan::Client
  def initialize(options = {})
    @options = options
    @options[:identifier] ||= Gratan::Identifier::Null.new
    client = Mysql2::Client.new(options)
    @driver = Gratan::Driver.new(client, options)
  end

  def export(options = {})
    options = @options.merge(options)
    exported = Gratan::Exporter.export(@driver, options)

    if block_given?
      exported.sort_by {|user_host, attrs|
        user_host[0].empty? ? 'root' : user_host[0]
      }.chunk {|user_host, attrs|
        user_host[0].empty? ? 'root' : user_host[0]
      }.each {|user, grants|
        h = {}
        grants.each {|k, v| h[k] = v }
        dsl = Gratan::DSL.convert(h, options)
        yield(user, dsl)
      }
    else
      exported = Gratan::Exporter.export(@driver, options)
      Gratan::DSL.convert(exported, options)
    end
  end

  def apply(file, options = {})
    options = @options.merge(options)

    in_progress do
      walk(file, options)
    end
  end

  private

  def walk(file, options)
    expected = load_file(file, options)
    actual = Gratan::Exporter.export(@driver, options.merge(:with_identifier => true))

    expected.each do |user_host, expected_attrs|
      next if user_host[0] =~ options[:ignore_user]
      actual_attrs = actual.delete(user_host)

      if actual_attrs
        walk_user(*user_host, expected_attrs, actual_attrs)
      else
        create_user(*user_host, expected_attrs)
      end
    end

    actual.each do |user_host, attrs|
      next if user_host[0] =~ options[:ignore_user]
      drop_user(*user_host)
    end
  end

  def create_user(user, host, attrs)
    attrs[:options] ||= {}

    unless attrs.has_key?(:identified)
      identified = @options[:identifier].identify(user, host)
      attrs[:options][:identified] = identified if identified
    end

    @driver.create_user(user, host, attrs)
    update!
  end

  def drop_user(user, host)
    @driver.drop_user(user, host)
    update!
  end

  def walk_user(user, host, expected_attrs, actual_attrs)
    walk_options(user, host, expected_attrs[:options], actual_attrs[:options])
    walk_objects(user, host, expected_attrs[:objects], actual_attrs[:objects])
  end

  def walk_options(user, host, expected_options, actual_options)
    if expected_options.has_key?(:identified)
      walk_identified(user, host, expected_options[:identified], actual_options[:identified])
    end

    walk_required(user, host, expected_options[:required], actual_options[:required])
  end

  def walk_identified(user, host, expected_identified, actual_identified)
    if expected_identified != actual_identified
      @driver.identify(user, host, expected_identified)
      update!
    end
  end

  def walk_required(user, host, expected_required, actual_required)
    if expected_required != actual_required
      @driver.set_require(user, host, expected_required)
      update!
    end
  end

  def walk_objects(user, host, expected_objects, actual_objects)
    expected_objects.each do |object, expected_options|
      expected_options ||= {}
      actual_options = actual_objects.delete(object)

      if actual_options
        walk_object(user, host, object, expected_options, actual_options)
      else
        @driver.grant(user, host, object, expected_options)
        update!
      end
    end

    actual_objects.each do |object, options|
      options ||= {}
      @driver.revoke(user, host, object, options)
      update!
    end
  end

  def walk_object(user, host, object, expected_options, actual_options)
    walk_with_option(user, host, object, expected_options[:with], actual_options[:with])
    walk_privs(user, host, object, expected_options[:privs], actual_options[:privs])
  end

  def walk_with_option(user, host, object, expected_with_option, actual_with_option)
    expected_with_option = (expected_with_option || '').upcase
    actual_with_option = (actual_with_option || '').upcase

    if expected_with_option != actual_with_option
      @driver.update_with_option(user, host, object, expected_with_option)
      update!
    end
  end

  def walk_privs(user, host, object, expected_privs, actual_privs)
    expected_privs = normalize_privs(expected_privs)
    actual_privs = normalize_privs(actual_privs)

    revoke_privs = actual_privs - expected_privs
    grant_privs = expected_privs - actual_privs

    unless revoke_privs.empty?
      if revoke_privs.length == 1 and revoke_privs[0] == 'USAGE' and not grant_privs.empty?
        # nothing to do
      else
        @driver.revoke(user, host, object, :privs => revoke_privs)
        update!
      end
    end

    unless grant_privs.empty?
      @driver.grant(user, host, object, :privs => grant_privs)
      update!
    end
  end

  def normalize_privs(privs)
    privs.map do |priv|
      priv = priv.split('(', 2)
      priv[0].upcase!

      if priv[1]
        priv[1] = priv[1].split(',').map {|i| i.gsub(')', '').strip }.sort.join(', ')
        priv[1] << ')'
      end

      priv = priv.join('(')

      if priv == 'ALL'
        priv = 'ALL PRIVILEGES'
      end

      priv
    end
  end

  def load_file(file, options)
    if file.kind_of?(String)
      open(file) do |f|
        Gratan::DSL.parse(f.read, file, options)
      end
    elsif file.respond_to?(:read)
      Gratan::DSL.parse(file.read, file.path, options)
    else
      raise TypeError, "can't convert #{file} into File"
    end
  end

  def in_progress
    updated = false

    begin
      @driver.disable_log_bin_local
      @updated = false
      yield
      updated = @updated
    ensure
      @updated = nil
    end

    updated
  end

  def update!
    @updated = true unless @options[:dry_run]
  end
end