# frozen_string_literal: true

module Bolt
  class NoImplementationError < Bolt::Error
    def initialize(target, task)
      msg = "No suitable implementation of #{task.name} for #{target.name}"
      super(msg, 'bolt/no-implementation')
    end
  end

  # Represents a Task.
  # @file and @files are mutually exclusive.
  # @name [String] name of the task
  # @file [Hash, nil] containing `filename` and `file_content`
  # @files [Array<Hash>] where each entry includes `name` and `path`
  # @metadata [Hash] task metadata
  Task = Struct.new(
    :name,
    :file,
    :files,
    :metadata
  ) do
    attr_reader :remote

    def initialize(task, remote: false)
      super(nil, nil, [], {})

      @remote = remote

      task.reject { |k, _| k == 'parameters' }.each { |k, v| self[k] = v }
    end

    def remote_instance
      self.class.new(to_h.each_with_object({}) { |(k, v), h| h[k.to_s] = v }, remote: true)
    end

    def description
      metadata['description']
    end

    def parameters
      metadata['parameters']
    end

    def supports_noop
      metadata['supports_noop']
    end

    def module_name
      name.split('::').first
    end

    def tasks_dir
      File.join(module_name, 'tasks')
    end

    def file_map
      @file_map ||= files.each_with_object({}) { |file, hsh| hsh[file['name']] = file }
    end
    private :file_map

    # This provides a method we can override in subclasses if the 'path' needs
    # to be fetched or computed.
    def file_path(file_name)
      file_map[file_name]['path']
    end

    def implementations
      metadata['implementations']
    end

    # Returns a hash of implementation name, path to executable, input method (if defined),
    # and any additional files (name and path)
    def select_implementation(target, provided_features = [])
      impl = if (impls = implementations)
               available_features = target.features + provided_features
               impl = impls.find do |imp|
                 remote_impl = imp['remote']
                 remote_impl = metadata['remote'] if remote_impl.nil?
                 Set.new(imp['requirements']).subset?(available_features) && !!remote_impl == @remote
               end
               raise NoImplementationError.new(target, self) unless impl
               impl = impl.dup
               impl['path'] = file_path(impl['name'])
               impl.delete('requirements')
               impl
             else
               raise NoImplementationError.new(target, self) unless !!metadata['remote'] == @remote
               name = files.first['name']
               { 'name' => name, 'path' => file_path(name) }
             end

      inmethod = impl['input_method'] || metadata['input_method']
      impl['input_method'] = inmethod unless inmethod.nil?

      mfiles = impl.fetch('files', []) + metadata.fetch('files', [])
      dirnames, filenames = mfiles.partition { |file| file.end_with?('/') }
      impl['files'] = filenames.map do |file|
        path = file_path(file)
        raise "No file found for reference #{file}" if path.nil?
        { 'name' => file, 'path' => path }
      end

      unless dirnames.empty?
        files.each do |file|
          name = file['name']
          if dirnames.any? { |dirname| name.start_with?(dirname) }
            impl['files'] << { 'name' => name, 'path' => file_path(name) }
          end
        end
      end

      impl
    end
  end
end