class Msf::Analyze

  def initialize(framework)
    @framework = framework
  end

  def host(eval_host, payloads: nil)
    unless eval_host.vulns
      return {}
    end

    # Group related vulns
    vuln_families = group_vulns(eval_host.vulns)

    # finds all modules that have references matching those found on host vulns with service data
    suggested_modules = suggest_modules_for_vulns(eval_host, vuln_families, payloads: payloads)

    {results: suggested_modules}
  end

  private

  def group_vulns(vulns)
    return [] if vulns.empty?

    vulns = vulns.map do |vuln|
      [vuln, Set.new(vuln.refs.map {|r| r.name.upcase})]
    end
    grouped_vulns = Hash.new

    vulns.each_index do |ii|
      vuln, refs = vulns[ii]
      grouped_vulns[vuln] ||= [Set.new, Set.new]
      grouped_vulns[vuln][0] << vuln
      grouped_vulns[vuln][1].merge(refs)

      vulns[(ii+1)..-1].each do |candidate_match, candidate_refs|
        # TODO: measure if sorting the refs ahead of time and doing a O(n + m)
        # walk here is faster
        if candidate_refs.intersect? refs
          if grouped_vulns[candidate_match]
            # For merging two different transitive sets that overlap, we need
            # to merge the individual grouping elements and then use those
            # cells inside both the grouping arrays so that all the
            # already-visited vulns are rolled into the big new set
            grouped_vulns[candidate_match][0] = grouped_vulns[candidate_match][0].merge(grouped_vulns[vuln][0])
            grouped_vulns[candidate_match][1] = grouped_vulns[candidate_match][1].merge(grouped_vulns[vuln][1])
            grouped_vulns[vuln][0] = grouped_vulns[candidate_match][0]
            grouped_vulns[vuln][1] = grouped_vulns[candidate_match][1]
          else
            # Whoever was initialized first has the canonical set
            grouped_vulns[candidate_match] = grouped_vulns[vuln]
          end
        end
      end
    end

    vuln_families = grouped_vulns.values
    vuln_families.uniq!
    vuln_families
  end

  def suggest_modules_for_vulns(eval_host, vuln_families, payloads: nil)
    mrefs, _mports, _mservs = Msf::Modules::Metadata::Cache.instance.all_exploit_maps
    suggested_modules = []

    evaluated_module_targets = Set.new
    to_evaluate_with_defaults = Array.new
    vuln_families.each do |vulns, refs|
      found_modules = mrefs.values_at(*refs).compact.reduce(:+)
      next unless found_modules

      services = vulns.map(&:service).compact
      found_modules.each do |fnd_mod|
        if services.any?
          services.each do |svc|
            port = svc.port
            next if evaluated_module_targets.include?([fnd_mod, port])

            creds = @framework.db.creds(svcs: [svc.name], workspace: eval_host.workspace)
            r = Result.new(mod: fnd_mod, host: eval_host, datastore: {'rport': port},
                           available_creds: creds, payloads: payloads, framework: @framework)
            if r.match?
              suggested_modules << r.evaluate
            end
            evaluated_module_targets << [fnd_mod, port]
          end
        else
          # Only have the default port to go off of, at least for this vuln,
          # prefer using the service data if available on a different vuln
          # entry
          port = fnd_mod.rport
          to_evaluate_with_defaults << [fnd_mod, port]
        end
      end
    end

    to_evaluate_with_defaults.each do |fnd_mod, port|
      next if evaluated_module_targets.include?([fnd_mod, port])

      creds = @framework.db.creds(port: port, workspace: eval_host.workspace) if port
      r = Result.new(mod: fnd_mod, host: eval_host, datastore: {'rport': port},
                     available_creds: creds, payloads: payloads, framework: @framework)

      if r.match?
        suggested_modules << r.evaluate
      end
      evaluated_module_targets << [fnd_mod, port]
    end

    suggested_modules
  end
end
