# copyright: 2015, Dominik Richter
# Copyright © 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates.
# All Rights Reserved.

require "inspec/log"
require "inspec/plugin/v2"
require "inspec/utils/deprecated_cloud_resources_list"
require "inspec/utils/deprecated_core_resources_list"

module Inspec::DSL
  attr_accessor :backend

  def require_controls(id, version = nil, &block)
    opts = { profile_id: id, include_all: false, backend: @backend, conf: @conf, dependencies: @dependencies, profile_version: version }
    ::Inspec::DSL.load_spec_files_for_profile(self, opts, &block)
  end

  def include_controls(id, version = nil, &block)
    opts = { profile_id: id, include_all: true, backend: @backend, conf: @conf, dependencies: @dependencies, profile_version: version }
    ::Inspec::DSL.load_spec_files_for_profile(self, opts, &block)
  end

  alias require_rules require_controls
  alias include_rules include_controls

  def require_resource(options = {})
    raise "You must specify a specific resource name when calling require_resource()" if options[:resource].nil?

    from_profile = options[:profile] || profile_name
    target_name = options[:as] || options[:resource]
    res = resource_class(from_profile, options[:resource])
    add_resource(target_name, res)
  end

  ##
  # Try to load and instantiate a missing resource or raise LoadError
  # if unable. Requiring the resource registers it and generates a
  # method for it so you should only hit this once per missing
  # resource.

  def self.method_missing_resource(backend, id, *arguments)
    return unless backend

    begin
      require "inspec/resources/#{id}"
    rescue LoadError => e

      # InSpec 7+ Fallback Support for Resource Packs
      # Use Deprecator to lookup the matching gem
      # if a gem matches, try to load the resource from the gem
      gem_name = Inspec::Deprecation::Deprecator.new.match_gem_for_fallback_resource_name(id.to_s)
      if gem_name
        # Install if needed
        cfg = Inspec::Config.cached
        unless cfg.final_options[:auto_install_gems]
          Inspec.deprecate(:core_resource_moved_to_rp, "The resource pack gem '#{gem_name}' is required for resource '#{id}' support (consider --auto-install-gems).")

        end

        Inspec::Plugin::V2::Installer.instance.ensure_installed gem_name

        # Load the gem, add  gemspecs to the path, load any deps, load resource libraries into registry
        # This is plugin API orthodoxy but is impossible to program
        # Inspec::Plugin::V2::Registry.instance.find_activator(gem_name).activate
        loader = Inspec::Plugin::V2::Loader.new

        # 1. Activate gem and deps.
        loader.activate_managed_gems_for_plugin(gem_name)

        # 2. Load all libraries from the gem path
        gem_path = loader.find_gem_directory(gem_name)
        resources_path = File.join(gem_path, "lib", gem_name, "resources", "*.rb")
        legacy_library_path = File.join(gem_path, "libraries", "*.rb")
        Dir.glob([resources_path, legacy_library_path]).each do |resource_lib|
          require resource_lib
        end
        # Resources now available in Inspec::Resource.registry
      else
        # TODO: InSpec 7: Replace with Fallbacks for inspec-aws-resources and inspec-azure-resources
        include DeprecatedCloudResourcesList
        cloud_resource = id.start_with?("aws_") ? "aws" : "azure"

        # Deprecated AWS and Azure resources in InSpec 5.
        if CLOUD_RESOURCES_DEPRECATED.include? id
          Inspec.deprecate(:"#{cloud_resource}_resources_in_resource_pack", "Resource '#{id}'")
        else
          # OK, give up entirely
          raise LoadError, "#{e.message}"
        end
      end
    end

    klass = Inspec::Resource.registry[id.to_s]
    klass.new(backend, id, *arguments)
  end

  # Support for Outer Profile DSL plugins
  # This is called when an unknown method is encountered
  # "bare" in a control file - outside of a control or describe block.
  def method_missing(method_name, *arguments, &block)
    # Check to see if there is a outer_profile_dsl plugin activator hook with the method name
    registry = Inspec::Plugin::V2::Registry.instance
    hook = registry.find_activators(plugin_type: :outer_profile_dsl, activator_name: method_name).first
    if hook
      # OK, load the hook if it hasn't been already.  We'll then know a module,
      # which we can then inject into the context
      hook.activate
      # Inject the module's methods into the context
      # implementation_class is the field name, but this is actually a module.
      self.class.include(hook.implementation_class)
      # Now that the module is loaded, it defined one or more methods
      # (presumably the one we were looking for.)
      # We still haven't called it, so do so now.
      send(method_name, *arguments, &block)
    else
      begin
        Inspec::DSL.method_missing_resource(backend, method_name, *arguments)
      rescue LoadError
        super
      end
    end
  end

  def self.load_spec_files_for_profile(bind_context, opts, &block)
    dependencies = opts[:dependencies]
    profile_id = opts[:profile_id]
    profile_version = opts[:profile_version]

    new_profile_id = nil
    if profile_version
      new_profile_id = "#{profile_id}-#{profile_version}"
    else
      dependencies.list.each do |key, value|
        # 1. Fetching VERSION from a profile dependency name which is in a format NAME-VERSION.
        # 2. Matching original profile dependency name with profile name used with include or require control DSL.
        source_version = value.source_version
        unless source_version.nil? || source_version.empty?
          profile_id_key = key.split("-#{source_version}")[0]
          new_profile_id = key if profile_id_key == profile_id
        end
      end
    end
    # If dep profile does not contain a source version, key does not contain a version as well. In that case new_profile_id will be always nil and instead profile_id would be used to fetch profile from dependency list.
    dep_entry = new_profile_id ? dependencies.list[new_profile_id] : dependencies.list[profile_id]

    if dep_entry.nil?
      raise <<~EOF
        Cannot load '#{profile_id}' since it is not listed as a dependency of #{bind_context.profile_name}.

        Dependencies available from this context are:
            #{dependencies.list.keys.join("\n    ")}
      EOF
    end

    # Do not load any controls if the profile is not supported
    return unless dep_entry.profile.supports_platform?

    context = dep_entry.profile.runner_context
    # if we don't want all the rules, then just make 1 pass to get all rule_IDs
    # that we want to keep from the original
    if !opts[:include_all] || !(opts[:conf]["profile"].include_tags_list.empty?) || !opts[:conf]["profile"].include_controls_list.empty?
      filter_included_controls(context, dep_entry.profile, opts, &block)
    end
    # interpret the block and skip/modify as required
    context.load(block) if block_given?
    bind_context.add_subcontext(context)
  end

  def self.filter_included_controls(context, profile, opts, &block)
    mock = Inspec::Backend.create(Inspec::Config.mock)
    include_ctx = Inspec::ProfileContext.for_profile(profile, mock)
    include_ctx.load(block) if block_given?
    include_ctx.control_eval_context.conf = opts[:conf]
    control_eval_ctx = include_ctx.control_eval_context
    # remove all rules that were not registered
    context.all_rules.each do |r|
      id = Inspec::Rule.rule_id(r)
      fid = Inspec::Rule.profile_id(r) + "/" + id
      if !opts[:include_all] && !(include_ctx.rules[id] || include_ctx.rules[fid])
        context.remove_rule(fid)
      end

      unless control_eval_ctx.controls_list_empty?
        # filter the dependent profile controls which are not in the --controls options list
        context.remove_rule(fid) unless control_eval_ctx.control_exist_in_controls_list?(id)
      end

      unless control_eval_ctx.tags_list_empty?
        # filter included controls using --tags
        tag_ids = control_eval_ctx.control_tags(r)
        context.remove_rule(fid) unless control_eval_ctx.tag_exist_in_control_tags?(tag_ids)
      end
    end
  end
end
