require 'thor/shell'
require 'terminal-notifier'

module DockerSync
  module SyncStrategy
    class Rsync
      include Thor::Shell

      @options
      @sync_name
      @watch_thread

      def initialize(sync_name, options)
        @sync_name = sync_name
        @options = options
        # if a custom image is set, apply it
        if @options.key?('image')
          @docker_image = @options['image']
        else
          @docker_image = 'eugenmayer/rsync'
        end

        begin
          Dependencies::Rsync.ensure!
        rescue StandardError => e
          say_status 'error', "#{@sync_name} has been configured to sync with rsync, but no rsync or fswatch binary available", :red
          say_status 'error', e.message, :red
          exit 1
        end
      end

      def run
        start_container
        sync
      end

      def sync
        args = sync_options
        cmd = 'rsync ' + args.join(' ')

        say_status 'command', cmd, :white if @options['verbose']

        out = `#{cmd}`
        if $?.exitstatus > 0
          error_msg = "Error starting sync, exit code #{$?.exitstatus}"
          say_status 'error', error_msg, :red
          say_status 'message', out
          TerminalNotifier.notify(
            "#{error_msg}", :title => @sync_name, :subtitle => @options['src'], group: 'docker-sync'
          ) if @options['notify_terminal']
        else
          TerminalNotifier.notify(
            "Synced #{@options['src']}", :title => @sync_name, group: 'docker-sync'
          ) if @options['notify_terminal'] && @options['notify_terminal'] != 'errors_only'
          say_status 'ok', "Synced #{@options['src']}", :white
          if @options['verbose']
            say_status 'output', out
          end
        end
      end

      def sync_options
        args = []

        unless @options['sync_excludes'].nil?
          excludes_list = @options['sync_excludes'].append(Environment.default_ignores).flatten
          args = excludes_list.map { |pattern| "--exclude='#{pattern}'" }
        end
        args.push('-ap')
        args.push(@options['sync_args']) if @options.key?('sync_args')
        # we do not need to user usermap/groupmap since we started our container the way that it also maps user/group like we defined
        # in the config - see start_container
        #args.push("--usermap='*:#{@options['sync_user']}'") if @options.key?('sync_user')
        #args.push("--groupmap='*:#{@options['sync_group']}'") if @options.key?('sync_group')
        args.push("'#{@options['src']}'")
        args.push("rsync://#{@options['sync_host_ip']}:#{@options['sync_host_port']}/volume")
        return args
      end

      # sleep until rsync --dry-run succeeds in connecting to the daemon on the configured IP and port. 
      def health_check
        health_check_cmd = "rsync --dry-run rsync://#{@options['sync_host_ip']}:#{@options['sync_host_port']}:: 2> /dev/null"

        retry_counter = 0
        
        health_check_status = `#{health_check_cmd}`
        while health_check_status == '' && retry_counter < 10 do
          say_status 'ok', "waiting for rsync daemon on rsync://#{@options['sync_host_ip']}:#{@options['sync_host_port']}", :white if @options['verbose']
          retry_counter += 1
          health_check_status = `#{health_check_cmd}`
          sleep 1
        end
      end

      def get_container_name
        return "#{@sync_name}"
      end

      def get_volume_name
        return @sync_name
      end

      # starts a rsync docker container listening on the specific port
      # this container exposes a named volume and is on one side used as the rsync-endpoint for the
      # local rsync command, on the other side the volume is mounted into the app-container to share the code / content
      def start_container
        say_status 'ok', "Starting rsync for sync #{@sync_name}", :white

        container_name = get_container_name
        volume_name = get_volume_name
        running = `docker ps --filter 'status=running' --filter 'name=#{container_name}' --format "{{.Names}}" | grep '^#{container_name}$'`
        if running == '' # container is yet not running
          say_status 'ok', "#{container_name} container not running", :white if @options['verbose']
          exists = `docker ps --filter "status=exited" --filter "name=#{container_name}" --format "{{.Names}}" | grep '^#{container_name}$'`
          if exists == '' # container has yet not been created
            say_status 'ok', "creating #{container_name} container", :white if @options['verbose']

            user_mapping = get_user_mapping
            group_mapping = get_group_mapping
            docker_env = get_docker_env

            cmd = "docker run -p '#{@options['sync_host_port']}:873' -v #{volume_name}:#{@options['dest']} #{user_mapping} #{group_mapping} #{docker_env} --name #{container_name} -d #{@docker_image}"
          else # container already created, just start / reuse it
            say_status 'ok', "starting #{container_name} container", :white if @options['verbose']
            cmd = "docker start #{container_name}"
          end
          say_status 'command', cmd, :white if @options['verbose']
          `#{cmd}` || raise('Start failed')
        else
          say_status 'ok', "#{container_name} container still running", :blue if @options['verbose']
        end
        say_status 'ok', "#{container_name}: starting initial sync of #{@options['src']}", :white if @options['verbose']

        health_check
        sync
        say_status 'success', 'Rsync server started', :green
      end

      def get_user_mapping
        user_mapping = ''
        if @options.key?('sync_userid')
          user_mapping = "#{user_mapping} -e OWNER_UID=#{@options['sync_userid']}"
        end
        return user_mapping
      end

      def get_group_mapping
        group_mapping = ''
        if @options.key?('sync_groupid')
          raise 'for now, rsync does no longer support groupid, but for nearly all cases sync_userid should be enough'
          #group_mapping = "#{group_mapping} -e GROUP_ID=#{@options['sync_groupid']}"
        end
        return group_mapping
      end

      def get_docker_env
        env_mapping = []
        env_mapping << "-e VOLUME=#{@options['dest']}"
        env_mapping << "-e TZ=$(basename $(dirname `readlink /etc/localtime`))/$(basename `readlink /etc/localtime`)"
        env_mapping << "-e ALLOW=#{@options['sync_host_allow']}" if @options['sync_host_allow']
        env_mapping.join(' ')
      end

      def stop_container
        `docker ps | grep #{get_container_name} && docker stop #{get_container_name}`
      end

      def reset_container
        stop_container
        `docker ps -a | grep #{get_container_name} && docker rm #{get_container_name}`
        `docker volume ls -q | grep #{get_volume_name} && docker volume rm #{get_volume_name}`
      end

      def clean
        reset_container
      end

      def stop
        say_status 'ok', "Stopping sync container #{get_container_name}"
        begin
          stop_container
        rescue StandardError => e
          say_status 'error', "Stopping failed of #{get_container_name}:", :red
          puts e.message
        end
      end
    end
  end
end
