# encoding: UTF-8
# frozen_string_literal: true

require 'fileutils'
require 'shellwords'

require 'dump/continious_timeout'
require 'dump/env'

require 'active_support/core_ext/object/blank'

require 'English'

Capistrano::Configuration.instance(:i_need_this!).load do
  namespace :dump do
    def dump_command(command, env = {})
      rake = env.delete(:rake) || 'rake'

      # stringify_keys! from activesupport
      Dump::Env.stringify!(env)

      env.update(Dump::Env.for_command(command, true))

      cmd = %W[-s dump:#{command}]
      cmd += env.sort.map{ |key, value| "#{key}=#{value}" }
      "#{rake} #{cmd.shelljoin}"
    end

    def fetch_rails_env
      fetch(:rails_env, 'production')
    end

    def got_rsync?
      `which rsync`
      $CHILD_STATUS.success?
    end

    def do_transfer_via(via, direction, from, to)
      case via
      when :rsync
        if run_local('which rsync').present? && $CHILD_STATUS.success?
          execute_on_servers do |servers|
            commands = servers.map do |server|
              target = sessions[server]
              user = target.options[:user] || fetch(:user, nil)
              host = target.host
              port = target.options[:port]
              full_host = "#{"#{user}@" if user.present?}#{host}"

              ssh = port.present? ? "ssh -p #{port}" : 'ssh'
              cmd = %W[rsync -P -e #{ssh} --timeout=15]
              case direction
              when :up
                cmd << from << "#{full_host}:#{to}"
              when :down
                cmd << "#{full_host}:#{from}" << to
              else
                fail "Don't know how to transfer in direction #{direction}"
              end
              cmd.shelljoin
            end
            commands.each do |cmd|
              logger.info cmd if logger

              3.times do
                break if system(cmd)
                break unless [10, 11, 12, 23, 30, 35].include?($CHILD_STATUS.exitstatus)
              end
              fail "rsync returned #{$CHILD_STATUS.exitstatus}" unless $CHILD_STATUS.success?
            end
          end
        end
      when :sftp, :scp
        Dump::ContiniousTimeout.timeout 15 do |thread|
          transfer(direction, from, to, :via => via) do |_channel, _path, transfered, total|
            thread.defer
            progress = if transfered < total
              format("\e[1m%5.1f%%\e[0m", transfered * 100.0 / total)
            else
              '100%'
            end
            $stderr << "\rTransfering: #{progress}"
          end
        end
      else
        fail "Unknown transfer method #{via}"
      end
    end

    def do_transfer(direction, from, to)
      via = Dump::Env[:transfer_via]
      case via && via.downcase
      when nil
        if got_rsync?
          do_transfer_via(:rsync, direction, from, to)
        else
          $stderr.puts 'To transfer using rsync — make rsync binary accessible and verify that remote host can work with rsync through ssh'
          begin
            do_transfer_via(:sftp, direction, from, to)
          rescue => e
            $stderr.puts e
            do_transfer_via(:scp, direction, from, to)
          end
        end
      when 'rsync'
        do_transfer_via(:rsync, direction, from, to)
      when 'sftp'
        do_transfer_via(:sftp, direction, from, to)
      when 'scp'
        do_transfer_via(:scp, direction, from, to)
      else
        fail "Unknown transfer method #{via}"
      end
    end

    def with_additional_tags(*tags, &block)
      tags = [tags, Dump::Env[:tags]].flatten.select(&:present?).join(',')
      Dump::Env.with_env(:tags => tags, &block)
    end

    def print_and_return_or_fail
      out = yield
      fail 'Failed creating dump' if out.blank?

      print out
      out.strip
    end

    def run_local(cmd)
      `#{cmd}`
    end

    def run_remote(cmd)
      output = ''
      run(cmd) do |_channel, io, data|
        case io
        when :out
          output << data
        when :err
          $stderr << data
        end
      end
      output
    end

    def last_part_of_last_line(out)
      line = out.strip.split(/\s*[\n\r]\s*/).last
      line.split("\t").last if line
    end

    def fetch_rake
      fetch(:rake, nil)
    end

    def auto_backup?
      !Dump::Env.no?(:backup)
    end

    namespace :local do
      desc "Shorthand for dump:local:create#{Dump::Env.explain_variables_for_command(:create)}"
      task :default, :roles => :db, :only => {:primary => true} do
        local.create
      end

      desc "Create local dump#{Dump::Env.explain_variables_for_command(:create)}"
      task :create, :roles => :db, :only => {:primary => true} do
        print_and_return_or_fail do
          with_additional_tags('local') do
            run_local(dump_command(:create))
          end
        end
      end

      desc "Restore local dump#{Dump::Env.explain_variables_for_command(:restore)}"
      task :restore, :roles => :db, :only => {:primary => true} do
        run_local(dump_command(:restore))
      end

      desc "Versions of local dumps#{Dump::Env.explain_variables_for_command(:versions)}"
      task :versions, :roles => :db, :only => {:primary => true} do
        print run_local(dump_command(:versions, :show_size => true))
      end

      desc "Cleanup local dumps#{Dump::Env.explain_variables_for_command(:cleanup)}"
      task :cleanup, :roles => :db, :only => {:primary => true} do
        print run_local(dump_command(:cleanup))
      end

      desc "Upload dump#{Dump::Env.explain_variables_for_command(:transfer)}"
      task :upload, :roles => :db, :only => {:primary => true} do
        file = Dump::Env.with_env(:summary => nil) do
          last_part_of_last_line(run_local(dump_command(:versions)))
        end
        if file
          do_transfer :up, "dump/#{file}", "#{current_path}/dump/#{file}"
        end
      end
    end

    namespace :remote do
      desc "Shorthand for dump:remote:create#{Dump::Env.explain_variables_for_command(:create)}"
      task :default, :roles => :db, :only => {:primary => true} do
        remote.create
      end

      desc "Create remote dump#{Dump::Env.explain_variables_for_command(:create)}"
      task :create, :roles => :db, :only => {:primary => true} do
        print_and_return_or_fail do
          with_additional_tags('remote') do
            run_remote("cd #{current_path}; #{dump_command(:create, :rake => fetch_rake, :RAILS_ENV => fetch_rails_env, :PROGRESS_TTY => '+')}")
          end
        end
      end

      desc "Restore remote dump#{Dump::Env.explain_variables_for_command(:restore)}"
      task :restore, :roles => :db, :only => {:primary => true} do
        run_remote("cd #{current_path}; #{dump_command(:restore, :rake => fetch_rake, :RAILS_ENV => fetch_rails_env, :PROGRESS_TTY => '+')}")
      end

      desc "Versions of remote dumps#{Dump::Env.explain_variables_for_command(:versions)}"
      task :versions, :roles => :db, :only => {:primary => true} do
        print run_remote("cd #{current_path}; #{dump_command(:versions, :rake => fetch_rake, :RAILS_ENV => fetch_rails_env, :PROGRESS_TTY => '+', :show_size => true)}")
      end

      desc "Cleanup of remote dumps#{Dump::Env.explain_variables_for_command(:cleanup)}"
      task :cleanup, :roles => :db, :only => {:primary => true} do
        print run_remote("cd #{current_path}; #{dump_command(:cleanup, :rake => fetch_rake, :RAILS_ENV => fetch_rails_env, :PROGRESS_TTY => '+')}")
      end

      desc "Download dump#{Dump::Env.explain_variables_for_command(:transfer)}"
      task :download, :roles => :db, :only => {:primary => true} do
        file = Dump::Env.with_env(:summary => nil) do
          last_part_of_last_line(run_remote("cd #{current_path}; #{dump_command(:versions, :rake => fetch_rake, :RAILS_ENV => fetch_rails_env, :PROGRESS_TTY => '+')}"))
        end
        if file
          FileUtils.mkpath('dump')
          do_transfer :down, "#{current_path}/dump/#{file}", "dump/#{file}"
        end
      end
    end

    desc "Shorthand for dump:local:upload#{Dump::Env.explain_variables_for_command(:transfer)}"
    task :upload, :roles => :db, :only => {:primary => true} do
      local.upload
    end

    desc "Shorthand for dump:remote:download#{Dump::Env.explain_variables_for_command(:transfer)}"
    task :download, :roles => :db, :only => {:primary => true} do
      remote.download
    end

    namespace :mirror do
      desc "Creates local dump, uploads and restores on remote#{Dump::Env.explain_variables_for_command(:mirror)}"
      task :up, :roles => :db, :only => {:primary => true} do
        auto_backup = if auto_backup?
          with_additional_tags('auto-backup') do
            remote.create
          end
        end
        if !auto_backup? || auto_backup.present?
          file = with_additional_tags('mirror') do
            local.create
          end
          if file.present?
            Dump::Env.with_clean_env(:like => file) do
              local.upload
              remote.restore
            end
          end
        end
      end

      desc "Creates remote dump, downloads and restores on local#{Dump::Env.explain_variables_for_command(:mirror)}"
      task :down, :roles => :db, :only => {:primary => true} do
        auto_backup = if auto_backup?
          with_additional_tags('auto-backup') do
            local.create
          end
        end
        if !auto_backup? || auto_backup.present?
          file = with_additional_tags('mirror') do
            remote.create
          end
          if file.present?
            Dump::Env.with_clean_env(:like => file) do
              remote.download
              local.restore
            end
          end
        end
      end
    end

    namespace :backup do
      desc "Shorthand for dump:backup:create#{Dump::Env.explain_variables_for_command(:backup)}"
      task :default, :roles => :db, :only => {:primary => true} do
        backup.create
      end

      desc "Creates remote dump and downloads to local (desc defaults to 'backup')#{Dump::Env.explain_variables_for_command(:backup)}"
      task :create, :roles => :db, :only => {:primary => true} do
        file = with_additional_tags('backup') do
          remote.create
        end
        if file.present?
          Dump::Env.with_clean_env(:like => file) do
            remote.download
          end
        end
      end

      desc "Uploads dump with backup tag and restores it on remote#{Dump::Env.explain_variables_for_command(:backup_restore)}"
      task :restore, :roles => :db, :only => {:primary => true} do
        file = with_additional_tags('backup') do
          last_part_of_last_line(run_local(dump_command(:versions)))
        end
        if file.present?
          Dump::Env.with_clean_env(:like => file) do
            local.upload
            remote.restore
          end
        end
      end
    end
  end

  after 'deploy:update_code' do
    from, to = %W[#{shared_path}/dump #{release_path}/dump]
    run "mkdir -p #{from}; rm -rf #{to}; ln -s #{from} #{to}"
  end
end
