require "inspec/utils/parser"
require "inspec/utils/filter"
require "inspec/utils/file_reader"

module Inspec::Resources
  class XinetdConf < Inspec.resource(1)
    name "xinetd_conf"
    supports platform: "unix"
    desc "Xinetd services configuration."
    example <<~EXAMPLE
      describe xinetd_conf.services('chargen') do
        its('socket_types') { should include 'dgram' }
      end

      describe xinetd_conf.services('chargen').socket_types('dgram') do
        it { should be_disabled }
      end
    EXAMPLE

    include Inspec::Utils::XinetdParser
    include FileReader

    def initialize(conf_path = "/etc/xinetd.conf")
      @conf_path = conf_path
      @contents = {}
      read_content(@conf_path)
    end

    def to_s
      "Xinetd config #{@conf_path}#{@filters}"
    end

    def params
      @params ||= read_params
    end

    filter = FilterTable.create
    filter.register_column(:services, field: "service")
      .register_column(:ids,          field: "id")
      .register_column(:socket_types, field: "socket_type")
      .register_column(:types,        field: "type")
      .register_column(:protocols,    field: "protocol")
      .register_column(:wait,         field: "wait")
      .register_custom_matcher(:disabled?) { |x| x.where("disable" => "no").services.empty? }
      .register_custom_matcher(:enabled?) { |x| x.where("disable" => "yes").services.empty? }
      .install_filter_methods_on_resource(self, :service_lines)

    private

    def read_content(path = @conf_path)
      return @contents[path] if @contents.key?(path)

      @contents[path] = read_file_content(path)
    end

    def read_params
      return {} if read_content.nil?

      flat_params = parse_xinetd(read_content)
      # we need to map service data in order to use it with filtertable
      params = { "services" => {} }
      # map services that were defined and map it to the service hash
      flat_params.each do |k, v|
        name = k[/^service (.+)$/, 1]
        # its not a service, no change required
        if name.nil?
          params[k] = v
        # handle service entries
        else
          # store service
          params["services"][name] = v

          # add the service identifier to its parameters
          if v.is_a?(Array)
            v.each { |service| service.params["service"] = name }
          else
            v.params["service"] = name
          end
        end
      end
      params
    end

    # Method used to derive the default protocol used from the socket_type
    def default_protocol(type)
      case type
      when "stream"
        "tcp"
      when "dgram"
        "udp"
      else
        "unknown"
      end
    end

    def service_lines
      @services ||= params["services"].values.flatten.map do |service|
        service.params["protocol"] ||= default_protocol(service.params["socket_type"])
        service.params
      end
    end
  end
end
