# frozen_string_literal: true

module Gitlab
  module Backup
    module Cli
      module Targets
        class Database < Target
          # Owner can read/write, group no permission, others no permission
          FILE_PERMISSION = 0o600

          IGNORED_ERRORS = [
            # Ignore warnings
            /WARNING:/,
            # Ignore the DROP errors; recent database dumps will use --if-exists with pg_dump
            /does not exist$/,
            # User may not have permissions to drop extensions or schemas
            /must be owner of/
          ].freeze
          IGNORED_ERRORS_REGEXP = Regexp.union(IGNORED_ERRORS).freeze

          # Rake task used to drop all tables from GitLab databases
          # This task is executed before restoring data
          DROP_TABLES_TASK = "gitlab:db:drop_tables"

          attr_reader :errors

          def initialize(context)
            super(context)

            @errors = []
          end

          def dump(destination_dir)
            FileUtils.mkdir_p(destination_dir)

            postgres = Gitlab::Backup::Cli::Services::Postgres.new(context)

            # Phase 1: trigger snapshot generation (quick)
            postgres.each do |database|
              dump_file_name = file_name(destination_dir, database)

              database.export_snapshot!
            rescue ActiveRecord::ConnectionNotEstablished
              raise Errors::DatabaseBackupError.new(database.connection_params, dump_file_name)
            end

            # Phase 2: Run PgDump based on snapshot_id (slow)
            postgres.each do |database| # rubocop:disable Style/CombinableLoops -- export needs to happen first
              pg_database_name = database.configuration.database
              dump_file_name = file_name(destination_dir, database)

              Gitlab::Backup::Cli::Output.print_info("Dumping PostgreSQL database #{pg_database_name} ... ")

              status = database_dump(database: database, filepath: dump_file_name)
              report_finish_status(status.success?)

              unless errors.empty?
                Gitlab::Backup::Cli::Output.error "------ BEGIN ERRORS -----"
                Gitlab::Backup::Cli::Output.print(errors.join, stderr: true)
                Gitlab::Backup::Cli::Output.error "------ END ERRORS -------"
              end

              raise Errors::DatabaseBackupError.new(database.connection_params, dump_file_name) unless status.success?
            ensure
              database.release_snapshot!
              database.restore_timeouts!
            end
          end

          def restore(source)
            databases = Gitlab::Backup::Cli::Services::Postgres.new(context)

            # Drop all tables Load the schema to ensure we don't have any newer tables
            # hanging out from a failed upgrade
            drop_tables!

            databases.each do |db|
              database_name = db.configuration.name
              pg_database_name = db.configuration.database
              db_file_name = file_name(source, db)

              Gitlab::Backup::Cli::Output.info("Restoring #{database_name} database ... ")

              unless db_file_name.exist?
                failure_info = "Database backup file '#{db_file_name}' for the #{database_name} database does not exist"

                if main_database?(db)
                  Gitlab::Backup::Cli::Output.print_tag(:failure)

                  raise Gitlab::Backup::Cli::Error, failure_info
                end

                Gitlab::Backup::Cli::Output.print_tag(:skipped)

                Gitlab::Backup::Cli::Output.warning(failure_info)

                next
              end

              Gitlab::Backup::Cli::Output.info "Restoring PostgreSQL database #{pg_database_name} ... "

              status = restore_tables(database: db, filepath: db_file_name)
              report_finish_status(status.success?)

              unless errors.empty?
                Gitlab::Backup::Cli::Output.error "------ BEGIN ERRORS -----"
                Gitlab::Backup::Cli::Output.print(errors.join, stderr: true)
                Gitlab::Backup::Cli::Output.error "------ END ERRORS -------"
              end

              unless status.success?
                raise Gitlab::Backup::Cli::Errors::DatabaseRestoreError.new(database.connection_params, db_file_name)
              end
            end
          end

          protected

          def main_database?(database)
            database.configuration.name.to_sym == :main
          end

          def file_name(base_dir, database)
            prefix = main_database?(database) ? '' : "#{database.configuration.name}_"

            Pathname.new(File.join(base_dir, "#{prefix}database.sql.gz"))
          end

          def ignore_error?(line)
            IGNORED_ERRORS_REGEXP.match?(line)
          end

          private

          def database_dump(database:, filepath:)
            pg_env = database.pg_env_variables
            pg_database_name = database.configuration.database

            pg_dump = ::Gitlab::Backup::Cli::Utils::PgDump.new(
              database_name: pg_database_name,
              snapshot_id: database.snapshot_id,
              env: pg_env).build_command

            pipeline = Gitlab::Backup::Cli::Shell::Pipeline.new(
              pg_dump,
              Utils::Compression.compression_command
            )

            pipeline.run!(output: [filepath, 'w', FILE_PERMISSION]).tap do |status|
              @errors = status.stderr
            end
          end

          def report_finish_status(status)
            Gitlab::Backup::Cli::Output.print_tag(status ? :success : :failure)
          end

          def drop_tables!
            Gitlab::Backup::Cli::Output.print_info "Cleaning existing databases ... "

            gitlab_path = context.gitlab_basepath

            # Drop existing tables from configured databases before restoring from a backup
            rake = Utils::Rake.new(DROP_TABLES_TASK, chdir: gitlab_path).execute

            unless rake.success?
              Gitlab::Backup::Cli::Output.print_tag(:failure)

              raise Errors::DatabaseCleanupError.new(task: DROP_TABLES_TASK, path: gitlab_path, error: rake.stderr)
            end

            Gitlab::Backup::Cli::Output.print_tag(:success)
            Gitlab::Backup::Cli::Output.info(rake.output) unless rake.output.empty?
          end

          def restore_tables(database:, filepath:)
            pipeline = Gitlab::Backup::Cli::Shell::Pipeline.new(
              Utils::Compression.decompression_command,
              pg_restore_cmd(database)
            )

            pipeline.run!(input: filepath).tap do |status|
              @errors = status.stderr
            end
          end

          def pg_restore_cmd(database)
            Shell::Command.new('psql', database.configuration.database, env: database.pg_env_variables)
          end
        end
      end
    end
  end
end
