require 'json'
require 'fileutils'
require 'mongo_oplog_backup/oplog'

module MongoOplogBackup
  class Restore
    attr_reader :config

    def backup_folder
      File.join(config.backup_dir)
    end

    def oplog_restore_folder
      File.join(backup_folder, 'tmp-restore')
    end

    def initialize(config)
      @config = config
    end

    # Given a directory of oplog dumps generated by the backup feature,
    # iteratively mongorestore them.
    # Mongorestore <3.4 expects a file named oplog.bson the directory specified
    # Mongorestore 3.4 adds support for --oplogFile parameter, which simplifies things.
    def restore_oplogs(dir, options={})
      default_restore_args = ['--stopOnError', '--oplogReplay']
      default_restore_args << '--noIndexRestore' if !!config.options[:noIndexRestore]
      if options[:oplogLimit]
        default_restore_args += ['--oplogLimit', options[:oplogLimit]]
        oplog_limit = BSON::Timestamp.from_string(options[:oplogLimit])
      end

      oplog_start_at = BSON::Timestamp.from_string(config.options[:oplogStartAt]) if config.options[:oplogStartAt]

      source_files = Oplog.find_oplogs(dir)

      validate_continuity!(source_files)

      MongoOplogBackup.log.debug "Starting oplog restore..."
      source_files.each do |filename|
        # TODO: mongorestore 3.4 supports --oplogFile

        unless oplog_start_at.nil?
          timestamps = Oplog.timestamps_from_filename(filename)
          if timestamps[:last] <= oplog_start_at
            MongoOplogBackup.log.debug "Skipping batch: #{filename}. Last op in batch (ts: #{timestamps[:last]}) is before oplogStartAt: #{oplog_start_at}"
            next
          end
        end
        unless oplog_limit.nil?
          timestamps = Oplog.timestamps_from_filename(filename)
          if timestamps[:first] > oplog_limit
            MongoOplogBackup.log.debug "Skipping batch: #{filename}. First op in batch (ts: #{timestamps[:first]}) is after oplogLimit: #{oplog_limit}"
            next
          end
        end

        temp_file_path = create_temp_oplog_dir(dir, filename)
        oplog_dir_path = File.dirname(temp_file_path)

        restore_args = default_restore_args.dup
        restore_args << '--gzip' if Oplog.gzip_fingerprint(filename)
        restore_args << oplog_dir_path
        status = config.mongorestore(restore_args).status
        if status != 0
          MongoOplogBackup.log.error("Mongorestore failed during oplog restore. Aborting. Exit code: #{status}")
          raise 'Oplog restore failed.'
        end

        begin
          File.unlink(temp_file_path)
          Dir.rmdir(oplog_dir_path)
        rescue SystemCallError => e
          MongoOplogBackup.log.error("Clean-up error for '#{temp_file_path}. #{e.message}")
          raise
        end
      end
      MongoOplogBackup.log.debug "Oplog restore complete."
    end

    def restore_dump(dir, options={})
      restore_args = ['--stopOnError']
      restore_args << '--noIndexRestore' if !!config.options[:noIndexRestore]
      restore_args << '--gzip' if config.use_compression?
      restore_args << File.join(dir, 'dump')

      MongoOplogBackup.log.debug "Starting full restore..."
      status = config.mongorestore(restore_args).status
      if status != 0
        MongoOplogBackup.log.error("Mongorestore failed.")
        raise 'Full restore failed.'
      end
      MongoOplogBackup.log.debug "Full restore complete."
    end

    def perform(mode, options={})
      if options[:oplogLimit]
        raise ArgumentError, "oplogLimit is not a timestamp: eg. <seconds>[:ordinal]" unless options[:oplogLimit] =~ /\A\d+(?::\d+)?\z/
      end

      if mode == :oplog
        restore_oplogs(backup_folder, options)
      elsif mode == :full
        restore_dump(backup_folder, options)
        restore_oplogs(backup_folder, options)
      end
    end

    private

    # Check for gaps in timestamps based only on the filename, trusting that the oplog set is complete (ie. the filename timestamps match the actual data)
    def validate_continuity!(source_files)
      expected_first = nil
      MongoOplogBackup.log.debug "Checking timestamps..."
      source_files.each do |filename|
        timestamps = Oplog.timestamps_from_filename(filename)
        raise("Filename without timestamps found in restore set: #{filename}") if timestamps.nil?
        if expected_first && expected_first != timestamps[:first]
          raise "Missing oplog dump? Expected a filename with first timestamp '#{expected_first}', but got '#{timestamps[:first]}' from filename '#{filename}'."
        end
        expected_first = timestamps[:last]
      end
      true
    end

    def create_temp_oplog_dir dir, filename
      temp_dir = File.join(dir, 'tmp-restore', File.basename(filename))
      temp_file_path = File.join(temp_dir, 'oplog.bson')
      FileUtils.mkdir_p(temp_dir)
      File.link(filename, temp_file_path)
      return temp_file_path
    rescue Errno::EEXIST => e
      # Probably, 'new_name' already exists when creating link.
      MongoOplogBackup.log.warn("Temporary oplog.bson link already exists: #{e.message}. Assuming valid.")
      return temp_file_path
    rescue SystemCallError => e
      MongoOplogBackup.log.error("Setup error for oplog '#{filename}' in '#{temp_file_path}")
      raise
    end
  end
end