require 'chef/mixins'
require 'erb'

# For testing purposes, if the first path cannot be found load the second
begin
  require_relative '../../omnibus-ctl/lib/gitlab_ctl'
rescue LoadError
  require_relative '../../gitlab-ctl-commands/lib/gitlab_ctl'
end

module Pgbouncer
  DEFAULT_RAILS_DATABASE = 'gitlabhq_production'.freeze

  class Databases
    attr_accessor :install_path, :databases, :options, :ini_file, :json_file, :template_file, :attributes, :userinfo, :groupinfo, :database
    attr_reader :data_path

    def self.rails_database(attributes: nil)
      attributes = GitlabCtl::Util.get_public_node_attributes if attributes.nil?

      if attributes.key?('gitlab')
        attributes.dig('gitlab', 'gitlab_rails', 'db_database') || attributes.dig('gitlab', 'gitlab-rails', 'db_database')
      else
        DEFAULT_RAILS_DATABASE
      end
    end

    def initialize(options, install_path, base_data_path)
      self.data_path = base_data_path
      @attributes = GitlabCtl::Util.get_public_node_attributes
      @install_path = install_path
      @options = options
      @ini_file = options['databases_ini'] || database_ini
      @json_file = options['databases_json'] || database_json
      @template_file = "#{@install_path}/embedded/cookbooks/gitlab-ee/templates/default/databases.ini.erb"
      @database = options['database'] || Databases.rails_database(attributes: @attributes)
      @databases = update_databases(JSON.load_file(@json_file)) if File.exist?(@json_file)
      @userinfo = GitlabCtl::Util.userinfo(options['host_user']) if options['host_user']
      @groupinfo = GitlabCtl::Util.groupinfo(options['host_group']) if options['host_group']
    end

    def update_databases(original = {})
      rails_databases = attributes.dig('gitlab', 'gitlab_rails', 'databases')&.values
      rails_databases = rails_databases ? rails_databases.uniq : [DEFAULT_RAILS_DATABASE]

      updated = {}

      original = rails_databases.to_h { |db| [db, {}] } if original.empty?

      original.each do |db, settings|
        settings.delete('password')

        updated[db] = ''

        settings['auth_user'] = settings.delete('user') if settings.key?('user')

        is_rails_db = rails_databases.include?(db)

        if db == @database || is_rails_db
          settings['host'] = options['newhost'] if options['newhost']
          settings['port'] = options['port'] if options['port']
          settings['auth_user'] = options['user'] if options['user']

          if is_rails_db
            settings['dbname'] = db
          elsif options['pg_database']
            settings['dbname'] = options['pg_database']
          end
        end

        settings.each do |setting, value|
          updated[db] << " #{setting}=#{value}"
        end

        updated[db].strip!
      end

      updated
    end

    def database_ini_template
      <<~EOF
        [databases]
        <% @databases.each do |db, settings| %>
        <%= db %> = <%= settings %>
        <% end %>
      EOF
    end

    def data_path=(path)
      full_path = "#{path}/pgbouncer"
      raise "The directory #{full_path} does not exist. Please ensure pgbouncer is configured on this node" unless Dir.exist?(full_path)

      @data_path = full_path
    end

    def render
      ERB.new(database_ini_template).result(binding)
    end

    def write
      File.open(@ini_file, 'w') do |file|
        file.puts render
        file.chown(userinfo.uid, groupinfo.gid) if options['host_user'] && options['host_group']
      end
    end

    def build_command_line
      psql = "#{install_path}/embedded/bin/psql"
      host = options['pg_host'] || listen_addr
      host = '127.0.0.1' if host.eql?('0.0.0.0')
      port = options['pg_port'] || listen_port
      "#{psql} -d pgbouncer -h #{host} -p #{port} -U #{options['user']}"
    end

    def pgbouncer_command(command)
      GitlabCtl::Util.get_command_output(
        "#{build_command_line} -c '#{command}'",
        options['host_user']
      )
    rescue GitlabCtl::Errors::ExecutionError => e
      $stderr.puts "Error running command: #{e}"
      $stderr.puts "ERROR: #{e.stderr}"
      raise
    end

    def show_databases
      pgbouncer_command('SHOW DATABASES')
    end

    def running?
      true if show_databases
    rescue GitlabCtl::Errors::ExecutionError
      false
    end

    def database_paused?
      return false unless running?

      databases = show_databases

      # Find the headings of the output from `SHOW DATABASES` to find out the location of the `paused` column
      headings = databases.lines.first.split("|").map(&:strip)
      paused_position = headings.index('paused')

      # Find the paused value of the specified database
      paused_status = databases.lines.find { |x| x.match(/#{@database}/) }.split('|')[paused_position].strip

      # 1 for paused and 0 for unpaused
      paused_status == "1"
    end

    def resume_if_paused
      pgbouncer_command("RESUME #{@database}") if database_paused?
    end

    def reload
      # Attempt to connect to the pgbouncer admin interface and send the RELOAD
      # command. Assumes the current user is in the list of admin-users
      pgbouncer_command('RELOAD')
    rescue GitlabCtl::Errors::ExecutionError
      $stderr.puts "There was an issue reloading pgbouncer through admin console"
      $stderr.puts "Check that gitlab-ctl write-pgpass has been run to populate ~/.pgpass for the Consul account."
      $stderr.puts "See https://docs.gitlab.com/ee/administration/postgresql/replication_and_failover.html#configure-pgbouncer-nodes"
    end

    def restart
      GitlabCtl::Util.get_command_output("gitlab-ctl restart pgbouncer")
    end

    def suspend
      pgbouncer_command('SUSPEND')
    end

    def resume
      pgbouncer_command('RESUME')
    end

    def kill
      pgbouncer_command("KILL #{options['pg_database']}")
    end

    def notify
      # If we haven't written databases.json yet, don't do anything
      return if databases.nil?

      write
      resume_if_paused
      begin
        reload
      rescue GitlabCtl::Errors::ExecutionError
        $stderr.puts "Unable to reload pgbouncer, restarting instead"
        restart
      end
    end

    def console
      exec(build_command_line)
    end

    private

    def database_ini
      attributes['pgbouncer']['databases_ini']
    end

    def database_json
      attributes['pgbouncer']['databases_json']
    end

    def listen_addr
      attributes['pgbouncer']['listen_addr']
    end

    def listen_port
      attributes['pgbouncer']['listen_port']
    end
  end
end
