require 'thor/shell'
require 'docker-sync/execution'
require 'docker-sync/update_check'
require 'open3'
require 'socket'
require 'terminal-notifier'

module DockerSync
  module SyncStrategy
    class NativeOsx
      include Thor::Shell
      include Execution

      @options
      @sync_name
      @watch_thread
      @local_server_pid

      def initialize(sync_name, options)
        @options = options
        @sync_name = sync_name
        # if a custom image is set, apply it
        @docker_image = @options.key?('image') ? @options['image'] : 'ghcr.io/eugenmayer/unison:2.52.1-4.12.0'

        begin
          Dependencies::Docker.ensure!
        rescue StandardError => e
          say_status 'error', "#{@sync_name} has been configured to sync with native docker volume, but docker is not found", :red
          say_status 'error', e.message, :red
          exit 1
        end
      end

      def start_container
        say_status 'ok', "Starting native_osx for sync #{@sync_name}", :white
        container_name = get_container_name
        host_sync_src = File.realpath(@options['src'])
        volume_app_sync_name = @sync_name
        env = {}
        raise 'sync_user is no longer supported, since it is not needed. Use sync_userid only please' if @options.key?('sync_user')

        env['UNISON_SRC'] = '/host_sync'
        env['UNISON_DEST'] = '/app_sync'

        env['UNISON_ARGS'] = ''
        if @options.key?('sync_args')
          sync_args = @options['sync_args']
          sync_args = @options['sync_args'].join(' ') if @options['sync_args'].kind_of?(Array)
          env['UNISON_ARGS'] = sync_args
        end

        ignore_strings = expand_ignore_strings
        env['UNISON_ARGS'] << ' ' << ignore_strings.join(' ')
        env['UNISON_ARGS'] << ' ' << sync_prefer
        env['UNISON_ARGS'] << ' -numericids -auto -batch'
        env['UNISON_WATCH_ARGS'] = '-repeat watch'

        env['MAX_INOTIFY_WATCHES'] = @options['max_inotify_watches'] if @options.key?('max_inotify_watches')
        if @options['sync_userid'] == 'from_host'
          env['OWNER_UID'] = Process.uid
        else
          env['OWNER_UID'] = @options['sync_userid'] if @options.key?('sync_userid')
        end

        monit_options = {
          monit_enable: 'MONIT_ENABLE',
          monit_interval: 'MONIT_INTERVAL',
          monit_high_cpu_cycles: 'MONIT_HIGH_CPU_CYCLES',
        }

        monit_options.each do |key, env_key|
          env[env_key] = @options[key.to_s] if @options.key?(key.to_s)
        end

        host_disk_mount_mode = '' # see https://github.com/moby/moby/pull/31047
        host_disk_mount_mode = ":#{@options['host_disk_mount_mode']}" if @options.key?('host_disk_mount_mode')

        additional_docker_env = env.map{ |key,value| "-e #{key}=\"#{value}\"" }.join(' ')
        running = `docker ps --filter 'status=running' --filter 'name=#{container_name}' --format "{{.Names}}" | grep '^#{container_name}$'`
        if 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 == ''
            say_status 'ok', "creating #{container_name} container", :white if @options['verbose']
            run_privileged = ''
            run_privileged = '--privileged' if @options.key?('max_inotify_watches') #TODO: replace by the minimum capabilities required
            tz_expression = '-e TZ=$(basename $(dirname `readlink /etc/localtime`))/$(basename `readlink /etc/localtime`)'
            say_status 'ok', 'Starting precopy', :white if @options['verbose']
            # we just run the precopy script and remove the container
            cmd = "docker run --rm -v \"#{volume_app_sync_name}:/app_sync\" -v \"#{host_sync_src}:/host_sync#{host_disk_mount_mode}\" -e HOST_VOLUME=/host_sync -e APP_VOLUME=/app_sync #{tz_expression} #{additional_docker_env} #{run_privileged} --name #{container_name} #{@docker_image} /usr/local/bin/precopy_appsync"
            say_status 'precopy', cmd, :white if @options['verbose']
            system(cmd) || raise('Precopy failed')
            say_status 'ok', 'Starting container', :white if @options['verbose']
            # this will be run below and start unison, since we did not manipulate CMD
            cmd = "docker run -d -v \"#{volume_app_sync_name}:/app_sync\" -v \"#{host_sync_src}:/host_sync#{host_disk_mount_mode}\" -e HOST_VOLUME=/host_sync -e APP_VOLUME=/app_sync #{tz_expression} #{additional_docker_env} #{run_privileged} --name #{container_name} #{@docker_image}"
          else
            say_status 'ok', "starting #{container_name} container", :white if @options['verbose']
            cmd1 = "docker start #{container_name}"
            cmd2 = "docker exec #{container_name} supervisorctl restart unison"
            cmd = "#{cmd1} && (#{cmd2} || (sleep 3 && #{cmd2})) || exit 1"
          end
        else
          say_status 'ok', "#{container_name} container still running, restarting unison in container", :blue
          cmd = "docker exec #{container_name} supervisorctl restart unison"
        end
        say_status 'command', cmd, :white if @options['verbose']
        system(cmd) || raise('Start failed')
        say_status 'ok', "starting initial sync of #{container_name}", :white if @options['verbose']
        # wait until container is started, then sync:
        say_status 'success', 'Sync container started', :green
      end

      def run
        start_container
        sync
      end

      def watch
        # nop
      end

      def sync
        # nop
      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

      def get_container_name
        @sync_name.to_s
      end

      private

      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 get_volume_name
        return @sync_name
      end

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


      # cares about conflict resolution
      def sync_prefer
        case @options.fetch('sync_prefer', 'default')
          when 'default' then
            '-prefer /host_sync' # thats our default, if nothing is set
          when 'src' then
            '-prefer /host_sync'
          when 'dest' then
            '-prefer /app_sync'
          when 'newer' then
            '-prefer newer'
          else
            raise 'sync_pref can only be: src or dest, no path - path is no longer needed it abstracted'
        end
      end

      def expand_ignore_strings
        expanded_ignore_strings = []
        default_expanded_ignore_strings = []

        exclude_type = 'Name'
        unless @options['sync_excludes_type'].nil?
          exclude_type = @options['sync_excludes_type']
        end

        # use the 'Name' exclude type for all default ignores
        # to prevent conflicts with the sync_excludes_type settings
        unless Environment.default_ignores.nil?
          default_expanded_ignore_strings = Environment.default_ignores.map do |pattern|
            "-ignore='Name #{pattern}'"
          end
        end
        
        unless @options['sync_excludes'].nil?
          expanded_ignore_strings = @options['sync_excludes'].map do |pattern|
            if exclude_type == 'none'
              # the ignore type like Name / Path are part of the pattern
              ignore_string = "#{pattern}"
            else
              ignore_string = "#{exclude_type} #{pattern}"
            end
            "-ignore='#{ignore_string}'"
          end
        end
        expanded_ignore_strings.append(default_expanded_ignore_strings).flatten!
      end
    end
  end
end
