require "singleton" unless defined?(Singleton)
require "json" unless defined?(JSON)
require "inspec/globals"

module Inspec::Plugin; end

module Inspec::Plugin::V2
  Exclusion = Struct.new(:plugin_name, :rationale)

  class PluginFilter
    include Singleton
    def initialize
      read_filter_data
    end

    def self.exclude?(plugin_name)
      instance.exclude?(plugin_name)
    end

    def exclude?(plugin_name)
      # Currently, logic is very simple: is there an exact match?
      # In the future, we might add regexes on names, or exclude version ranges
      return false unless @filter_data[:exclude].detect { |e| e.plugin_name == plugin_name }

      # OK, return entire data structure.
      @filter_data[:exclude].detect { |e| e.plugin_name == plugin_name }
    end

    private

    def read_filter_data
      path = File.join(Inspec.src_root, "etc", "plugin_filters.json")
      @filter_data = JSON.parse(File.read(path))

      unless @filter_data["file_version"] == "1.0.0"
        raise Inspec::Plugin::V2::ConfigError, "Unknown plugin fillter file format at #{path}"
      end

      validate_plugin_filter_file("1.0.0")

      @filter_data[:exclude] = @filter_data["exclude"].map do |entry|
        Exclusion.new(entry["plugin_name"], entry["rationale"])
      end
      @filter_data.delete("exclude")
    end

    def validate_plugin_filter_file(_file_version)
      unless @filter_data.key?("exclude") && @filter_data["exclude"].is_a?(Array)
        raise Inspec::Plugin::V2::ConfigError, 'Unknown plugin fillter file format: expected "exclude" to be an array'
      end

      @filter_data["exclude"].each_with_index do |entry, idx|
        unless entry.is_a? Hash
          raise Inspec::Plugin::V2::ConfigError, "Unknown plugin fillter file format: expected entry #{idx} to be a Hash / JS Object"
        end
        unless entry.key?("plugin_name")
          raise Inspec::Plugin::V2::ConfigError, "Unknown plugin fillter file format: expected entry #{idx} to have a \"plugin_name\" field"
        end
        unless entry.key?("rationale")
          raise Inspec::Plugin::V2::ConfigError, "Unknown plugin fillter file format: expected entry #{idx} to have a \"rationale\" field"
        end
      end
    end
  end

  # To be a valid plugin name, the plugin must beign with either
  # inspec- or train-, AND ALSO not be on the exclusion list.
  # We maintain this exclusion list to avoid confusing users.
  # For example, we want to have a real gem named inspec-test-fixture,
  # but we don't want the users to see that.
  module FilterPredicates
    def train_plugin_name?(name)
      valid_plugin_name?(name, :train)
    end

    def inspec_plugin_name?(name)
      valid_plugin_name?(name, :inspec)
    end

    def valid_plugin_name?(name, kind = :either)
      # Must have a permitted prefix.
      return false unless case kind
      when :inspec
        name.to_s.start_with?("inspec-")
      when :train
        name.to_s.start_with?("train-")
      when :either
        name.to_s.match(/^(inspec|train)-/)
      else false
      end # rubocop: disable Layout/EndAlignment

      # And must not be on the exclusion list.
      ! Inspec::Plugin::V2::PluginFilter.exclude?(name)
    end
  end
end
