# frozen_string_literal: true

require 'json'

class GitlabRbLoader
  attr_accessor :flat_hash

  class << self
    def audit(reference_rb, actual_rb)
      errors = []

      actual_rb.flat_hash.each do |name, actual|
        next unless reference_rb.flat_hash.key?(name)

        expected = reference_rb.flat_hash[name]
        e = GitlabRbLoader.audit_value(name, expected, actual)
        errors += e unless e.empty?
      end

      # Check for extract configs that do not exist in reference.
      extra_names = actual_rb.flat_hash.keys - reference_rb.flat_hash.keys
      unless extra_names.empty?
        errors << "ERROR: gitlab.rb has extraneous config values that do not exist in reference\n"
        extra_names.each { |n| errors << "  #{n} = #{actual_rb.flat_hash[n]}\n" }
      end

      missing_names = reference_rb.flat_hash.keys - actual_rb.flat_hash.keys
      unless missing_names.empty?
        errors << "ERROR: gitlab.rb missing config values that are specified in the reference:\n"
        missing_names.each { |n| errors << "  Missing: #{n} = #{reference_rb.flat_hash[n]}\n" }
      end

      errors
    end

    private

    def audit_value(name, expected, actual)
      errors = []

      if expected.is_a?(Array) && actual.is_a?(Array)
        audit_array(errors, name, expected, actual)
      else
        audit_type(errors, name, expected, actual)
      end

      errors
    end

    def audit_type(errors, name, expected, actual)
      return unless expected != actual

      errors << if expected.instance_of?(actual.class) || [true, false].include?(expected)
                  <<-MSG
          ERROR: #{name} mismatch:
            Reference: #{expected}
            Actual:    #{actual}
                  MSG
                else
                  <<-MSG
          ERROR: #{name} type mismatch
            Reference: #{expected} (type: #{expected.class})
            Actual:    #{actual} (type: #{actual.class})
                  MSG
                end
    end

    def audit_array(errors, name, expected, actual)
      # If arrays contanin hashes, compare them as they are.
      unless expected.empty? && expected.none? { |elem| elem.is_a?(Hash) }
        expected = expected.sort
        actual = actual.sort
      end

      return unless expected != actual

      message = <<-MSG
          ERROR: #{name} mismatch
            Reference [#{expected.length}]: '#{expected}'
            Actual    [#{actual.length}]: '#{actual}'
      MSG

      message += "  Missing: '#{expected - actual}'\n" unless (expected - actual).empty?
      message += "  Extra  : '#{actual - expected}'\n" unless (actual - expected).empty?

      errors << message
    end
  end

  def initialize(rb_file_path, cfg_ctx = nil)
    # puts "Loading #{rb_file_path}"
    @gitlab_rb_config = {}

    b = binding
    method_names = %w[external_url roles git_data_dirs pages_external_url]
    gitlab_rb_config_text = File.read(rb_file_path)

    # Get list of Hash variables with format: `gitlab_var["...` then remove the
    # method names from the list.
    var_names = gitlab_rb_config_text.scan(/^\s*([a-z0-9A-Z_]+)\s*\[/).flatten.uniq.reject do |vn|
      method_names.include?(vn)
    end

    # Create hash placeholders for each variable for receiving the key
    # assignments.
    var_names.each { |vn| eval("#{vn} = Hash.new", b, __FILE__, __LINE__) }

    # Inject configuration context variables __VAR__ and thier values.
    cfg_ctx&.vars_each do |n, v|
      # puts "Setting name: #{n} to Value #{v}"
      eval("#{n} = #{v}", b, __FILE__, __LINE__)
    end

    # Web nodes have nginx nested option. Need to figure out default dict equivalent later.
    b.eval("nginx['status'] = {'options' => Hash.new }") if var_names.include?('nginx')
    # SQL nodes have patroni nested option. Need to figure out default dict equivalent later.
    b.eval("patroni['postgresql'] = Hash.new ") if var_names.include?('patroni')

    #### BEGIN GITLAB_RB
    eval(gitlab_rb_config_text, b)
    #### END GITLAB_RB

    # Extract variables defined in gitlab_rb into gitlab_rb_config instance var.
    var_names.each { |vn| @gitlab_rb_config[vn] = eval(vn, b) }

    # Convert dict keys from symbols to strings.
    cfg = JSON.parse(@gitlab_rb_config.to_json).transform_keys(&:to_s)

    @flat_hash = generate_flat(cfg)
  end

  # BEGIN: methods called during the eval() of gitlab.rb
  def external_url(url)
    @gitlab_rb_config['external_url'] = url
  end

  def pages_external_url(url)
    @gitlab_rb_config['pages_external_url'] = url
  end

  def roles(roles)
    @gitlab_rb_config['roles'] = roles
  end

  def git_data_dirs(data_dirs)
    @gitlab_rb_config['git_data_dirs'] = data_dirs
  end
  # END: methods invoked called during the eval() of gitlab.rb

  private

  # Flatten a nested hash.
  # input:  { 'l1' => { 'l2.1' => 'value1', 'l2.2' => 'value2' }, }
  # output: { "l1['l2.1']" => 'value1', "l1['l2.2']" => 'value2' }
  #
  def generate_flat(nested_hash, prefix = '')
    raise "Not hash - #{nested_hash}/#{nested_hash.class}" unless nested_hash.is_a?(Hash)

    result_hash = {}

    nested_hash.each do |k, v|
      sub_prefix = prefix + (prefix == '' ? k : "['#{k}']")
      if v.is_a?(Hash)
        generate_flat(v, sub_prefix)
      else
        # puts "Adding keyname: #{sub_prefix} k: #{k} v: #{v}"
        result_hash[sub_prefix] = v
      end
    end

    result_hash
  end
end
