require 'optparse'
require 'mixlib/shellout'
require 'rainbow'

require_relative 'util'
require_relative '../gitlab_ctl'

# For testing purposes, if the first path cannot be found load the second
begin
  require_relative '../../../../cookbooks/gitlab/libraries/pg_version'
rescue LoadError
  require_relative '../../../gitlab-cookbooks/gitlab/libraries/pg_version'
end

module GitlabCtl
  class PgUpgrade
    include GitlabCtl::Util
    attr_accessor :base_path, :data_path, :tmp_dir, :timeout, :target_version, :initial_version, :psql_command, :port

    attr_writer :data_dir, :tmp_data_dir

    def initialize(base_path, data_path, target_version, tmp_dir = nil, timeout = nil, psql_command = nil, port = nil)
      @base_path = base_path
      @data_path = data_path
      @tmp_dir = tmp_dir
      @timeout = timeout
      @target_version = target_version
      @initial_version = fetch_running_version
      @port = port || public_node_attributes['postgresql']['port']
      @psql_command = psql_command || "gitlab-psql"
    end

    def data_dir
      return @data_dir if @data_dir

      # We still need to support legacy attributes starting with `gitlab`, as
      # they might exists before running configure on an existing installation
      pg_base_dir = node_attributes.dig(:gitlab, :postgresql, :dir) || node_attributes.dig(:postgresql, :dir) || File.join(@data_path, "postgresql")

      @data_dir = File.join(pg_base_dir, "data")

      @data_dir = File.realpath(@data_dir) if File.exist?(@data_dir)
      @data_dir
    end

    def tmp_data_dir
      return @tmp_data_dir if @tmp_data_dir

      @tmp_data_dir = @tmp_dir ? "#{@tmp_dir}/data" : data_dir
    end

    def enough_free_space?(dir, needed)
      needed <= space_free(dir)
    end

    def space_needed(dir)
      GitlabCtl::Util.get_command_output(
        "du -s --block-size=1m #{dir}", nil, @timeout
      ).split.first.to_i
    end

    def space_free(dir)
      space_available = GitlabCtl::Util.get_command_output(
        "df -P --block-size=1m #{dir} | awk '{print $4}'", nil, @timeout
      ).split.last.to_i

      (space_available * 0.9).to_i
    end

    def run_pg_command(command)
      pg_username = node_attributes.dig(:postgresql, :username)

      GitlabCtl::Util.get_command_output("su - #{pg_username} -c \"#{command}\"", nil, @timeout)
    end

    def fetch_running_version
      PGVersion.parse(GitlabCtl::Util.get_command_output(
        "#{@base_path}/embedded/bin/pg_ctl --version"
      ).split.last)
    end

    def run_query(query)
      GitlabCtl::Util.get_command_output(
        "#{@psql_command} -d postgres -c '#{query}' -q -t",
        nil, # user
        @timeout
      ).strip
    end

    def fetch_lc_collate
      run_query('SHOW LC_COLLATE')
    end

    def fetch_lc_ctype
      run_query('SHOW LC_CTYPE')
    end

    def fetch_server_encoding
      run_query('SHOW SERVER_ENCODING')
    end

    def fetch_data_version
      PGVersion.parse(File.read("#{data_dir}/PG_VERSION").strip)
    end

    def running?(service = 'postgresql')
      !GitlabCtl::Util.run_command("gitlab-ctl status #{service}").error?
    end

    def start(service = 'postgresql')
      GitlabCtl::Util.run_command("gitlab-ctl start #{service}").error!
    end

    def node_attributes
      @node_attributes ||= GitlabCtl::Util.get_node_attributes(@base_path)
    end

    def public_node_attributes
      @public_node_attributes ||= GitlabCtl::Util.get_public_node_attributes
    end

    def base_postgresql_path
      "#{base_path}/embedded/postgresql"
    end

    def target_version_path
      "#{base_postgresql_path}/#{target_version.major}"
    end

    def initial_version_path
      "#{base_postgresql_path}/#{initial_version.major}"
    end

    def upgrade_artifact_exists?(path)
      return false unless File.exist?(path)

      !Dir.empty?(path)
    end

    def log(message)
      $stderr.puts message
    end

    def run_pg_upgrade
      unless GitlabCtl::Util.progress_message('Upgrading the data') do
        begin
          run_pg_command(
            "#{target_version_path}/bin/pg_upgrade " \
            "-b #{initial_version_path}/bin " \
            "--old-datadir=#{data_dir}  " \
            "--new-datadir=#{tmp_data_dir}.#{target_version.major}  " \
            "-B #{target_version_path}/bin "
          )
        rescue GitlabCtl::Errors::ExecutionError => e
          log "Error upgrading the data to version #{target_version}"
          log "STDOUT: #{e.stdout}"
          log "STDERR: #{e.stderr}"
          false
        rescue Mixlib::ShellOut::CommandTimeout
          log ""
          log "Timed out during the database upgrade.".color(:red)
          log "To run with more time, remove the temporary directory #{tmp_data_dir}.#{target_version.major},".color(:red)
          log "then re-run your previous command, adding the --timeout option.".color(:red)
          log "See the docs for more information: https://docs.gitlab.com/omnibus/settings/database.html#upgrade-packaged-postgresql-server".color(:red)
          log "Or run gitlab-ctl pg-upgrade --help for usage".color(:red)
          false
        end
      end
        raise GitlabCtl::Errors::ExecutionError.new(
          'run_pg_upgrade', '', 'Error upgrading the database'
        )
      end
    end

    class << self
      def parse_options(args)
        options = {
          tmp_dir: nil,
          wait: true,
          skip_unregister: false,
          timeout: nil,
          target_version: nil,
          skip_disk_check: false,
          leader: nil,
          replica: nil,
          standby_leader: nil
        }

        OptionParser.new do |opts|
          opts.on('-tDIR', '--tmp-dir=DIR', 'Storage location for temporary data') do |t|
            options[:tmp_dir] = t
          end

          opts.on('-w', '--no-wait', 'Do not wait before starting the upgrade process') do
            options[:wait] = false
          end

          opts.on('-s', '--skip-unregister', 'Skip the attempt to unregister an HA secondary node. No-op in non-HA scenarios') do
            options[:skip_unregister] = true
          end

          opts.on('-TTIMEOUT', '--timeout=TIMEOUT', 'Timeout in milliseconds for the execution of the underlying commands. Accepts duration format such as 1d2h3m4s5ms.') do |t|
            i = GitlabCtl::Util.parse_duration(t)
            options[:timeout] = i.positive? ? i : nil
          end

          opts.on('-VVERSION', '--target-version=VERSION', 'The explicit major version to upgrade or downgrade to') do |v|
            options[:target_version] = v
          end

          opts.on('--skip-disk-check', 'Skip checking that there is enough free disk space to perform upgrade') do
            options[:skip_disk_check] = true
          end

          opts.on('--leader', 'Patroni only. Force leader upgrade procedure.') do
            options[:leader] = true
            options[:replica] = false
          end

          opts.on('--replica', 'Patroni only. Force replica upgrade procedure.') do
            options[:replica] = true
            options[:leader] = false
          end

          opts.on('--standby-leader', 'Patroni only. Force standby-leader upgrade procedure.') do
            options[:leader] = false
            options[:replica] = false
            options[:standby_leader] = true
          end
        end.parse!(args)

        options
      end
    end
  end
end
