require 'inspec-objects'
require 'word_wrap'
require 'pp'
require 'uri'
require 'net/http'
require 'fileutils'
require 'exceptions/impact_input_error'
require 'exceptions/severity_input_error'
require 'overrides/false_class'
require 'overrides/true_class'
require 'overrides/nil_class'
require 'overrides/object'
require 'overrides/string'
require 'rubocop'

module Utils
  class InspecUtil
    WIDTH = 80
    IMPACT_SCORES = {
      'none' => 0.0,
      'low' => 0.1,
      'medium' => 0.4,
      'high' => 0.7,
      'critical' => 0.9
    }.freeze

    def self.parse_data_for_ckl(json)
      data = {}

      # Parse for inspec profile results json
      json['profiles'].each do |profile|
        profile['controls'].each do |control|
          c_id = control['id'].to_sym
          data[c_id] = {}

          data[c_id][:vuln_num]       = control['id'] unless control['id'].nil?
          data[c_id][:rule_title]     = control['title'] unless control['title'].nil?
          data[c_id][:vuln_discuss]   = control['desc'] unless control['desc'].nil?

          unless control['tags'].nil?
            data[c_id][:severity]       = control['tags']['severity'] unless control['tags']['severity'].nil?
            data[c_id][:gid]            = control['tags']['gid'] unless control['tags']['gid'].nil?
            data[c_id][:group_title]    = control['tags']['gtitle'] unless control['tags']['gtitle'].nil?
            data[c_id][:rule_id]        = control['tags']['rid'] unless control['tags']['rid'].nil?
            data[c_id][:rule_ver]       = control['tags']['stig_id'] unless control['tags']['stig_id'].nil?
            data[c_id][:cci_ref]        = control['tags']['cci'] unless control['tags']['cci'].nil?
            data[c_id][:nist]           = control['tags']['nist'].join(' ') unless control['tags']['nist'].nil?
          end

          if control['descriptions'].respond_to?(:find)
            data[c_id][:check_content]  = control['descriptions'].find { |c| c['label'] == 'check' }&.dig('data')
            data[c_id][:fix_text]       = control['descriptions'].find { |c| c['label'] == 'fix' }&.dig('data')
          end

          data[c_id][:impact]         = control['impact'].to_s unless control['impact'].nil?
          data[c_id][:profile_name]   = profile['name'].to_s unless profile['name'].nil?
          data[c_id][:profile_shasum] = profile['sha256'].to_s unless profile['sha256'].nil?

          data[c_id][:status] = []
          data[c_id][:message] = []

          if control.key?('results')
            control['results'].each do |result|
              if !result['backtrace'].nil?
                result['status'] = 'error'
              end
              data[c_id][:status].push(result['status'])
              data[c_id][:message].push("SKIPPED -- Test: #{result['code_desc']}\nMessage: #{result['skip_message']}\n") if result['status'] == 'skipped'
              data[c_id][:message].push("FAILED -- Test: #{result['code_desc']}\nMessage: #{result['message']}\n") if result['status'] == 'failed'
              data[c_id][:message].push("PASS -- #{result['code_desc']}\n") if result['status'] == 'passed'
              data[c_id][:message].push("PROFILE_ERROR -- Test: #{result['code_desc']}\nMessage: #{result['backtrace']}\n") if result['status'] == 'error'
            end
          end

          if data[c_id][:impact].to_f.zero?
            data[c_id][:message].unshift("NOT_APPLICABLE -- Description: #{control['desc']}\n\n")
          end
        end
      end
      data
    end

    def self.get_platform(json)
      json['profiles'].find { |profile| !profile[:platform].nil? }
    end

    def self.to_dotted_hash(hash, recursive_key = '')
      hash.each_with_object({}) do |(k, v), ret|
        key = recursive_key + k.to_s
        if v.is_a? Hash
          ret.merge! to_dotted_hash(v, "#{key}.")
        else
          ret[key] = v
        end
      end
    end

    def self.control_status(control, for_summary = false)
      status_list = control[:status].uniq
      if control[:impact].to_f.zero?
        'Not_Applicable'
      elsif (status_list.include?('error') || status_list.empty?) && for_summary
        'Profile_Error'
      elsif status_list.include?('failed')
        'Open'
      elsif status_list.include?('passed')
        'NotAFinding'
      else
        # profile skipped or profile error
        'Not_Reviewed'
      end
    end

    def self.control_finding_details(control, control_clk_status)
      result = "One or more of the automated tests failed or was inconclusive for the control \n\n #{control[:message].sort.join}" if control_clk_status == 'Open'
      result = "All Automated tests passed for the control \n\n #{control[:message].join}" if control_clk_status == 'NotAFinding'
      result = "Automated test skipped due to known accepted condition in the control : \n\n#{control[:message].join}" if control_clk_status == 'Not_Reviewed'
      result = "Justification: \n #{control[:message].join}" if control_clk_status == 'Not_Applicable'
      result = 'No test available or some test errors occurred for this control' if control_clk_status == 'Profile_Error'
      result
    end

    # @!method get_impact(severity)
    #   Takes in the STIG severity tag and converts it to the InSpec #{impact}
    #   control tag.
    #   At the moment the mapping is static, so that:
    #     high => 0.7
    #     medium => 0.5
    #     low => 0.3
    # @param severity [String] the string value you want to map to an InSpec
    # 'impact' level.
    #
    # @return impact [Float] the impact level level mapped to the XCCDF severity
    # mapped to a float between 0.0 - 1.0.
    #
    # @todo Allow for the user to pass in a hash for the desired mapping of text
    # values to numbers or to override our hard coded values.
    #
    def self.get_impact(severity, use_cvss_terms: true)
      return float_to_impact(severity, use_cvss_terms) if severity.is_a?(Float)

      return string_to_impact(severity, use_cvss_terms) if severity.is_a?(String)

      raise SeverityInputError, "'#{severity}' is not a valid severity value. It should be a Float between 0.0 and " \
                                '1.0 or one of the approved keywords.'
    end

    private_class_method def self.float_to_impact(severity, use_cvss_terms)
      unless severity.between?(0, 1)
        raise SeverityInputError, "'#{severity}' is not a valid severity value. It should be a Float between 0.0 and " \
                                  '1.0 or one of the approved keywords.'
      end

      if severity <= 0.01
        0.0 # Informative
      elsif severity < 0.4
        0.3 # Low Impact
      elsif severity < 0.7
        0.5 # Medium Impact
      elsif severity < 0.9 || use_cvss_terms
        0.7 # High Impact
      else
        1.0 # Critical Controls
      end
    end

    private_class_method def self.string_to_impact(severity, use_cvss_terms)
      case severity
      when %r{none|na|n/a|not[_|(\s*)]?applicable}i
        impact = 0.0 # Informative
      when /low|cat(egory)?\s*(iii|3)/i
        impact = 0.3 # Low Impact
      when /med(ium)?|cat(egory)?\s*(ii|2)/i
        impact = 0.5 # Medium Impact
      when /high|cat(egory)?\s*(i|1)/i
        impact = 0.7 # High Impact
      when /crit(ical)?|severe/i
        impact = 1.0 # Critical Controls
      else
        raise SeverityInputError, "'#{severity}' is not a valid severity value. It should be a Float between 0.0 and " \
                                  '1.0 or one of the approved keywords.'
      end

      impact == 1.0 && use_cvss_terms ? 0.7 : impact
    end

    def self.get_impact_string(impact, use_cvss_terms: true)
      return if impact.nil?

      value = impact.to_f
      unless value.between?(0, 1)
        raise ImpactInputError, "'#{value}' is not a valid impact score. Valid impact scores: [0.0 - 1.0]."
      end

      IMPACT_SCORES.reverse_each do |name, impact_score|
        return 'high' if name == 'critical' && value >= impact_score && use_cvss_terms
        return name if value >= impact_score

        next
      end
    end

    def self.unpack_inspec_json(directory, inspec_json, separated, output_format)
      if directory == 'id'
        directory = inspec_json['name']
      end
      controls = generate_controls(inspec_json)
      unpack_profile(directory || 'profile', controls, separated, output_format || 'json')
      create_inspec_yml(directory || 'profile', inspec_json)
      create_license(directory || 'profile', inspec_json)
      create_readme_md(directory || 'profile', inspec_json)
    end

    private_class_method def self.wrap(str, width = WIDTH)
      str.gsub!("desc  \"\n    ", 'desc  "')
      str.gsub!(/\\r/, "\n")
      str.gsub!(/\\n/, "\n")

      WordWrap.ww(str.to_s, width)
    end

    private_class_method def self.generate_controls(inspec_json)
      controls = []
      inspec_json['controls'].each do |json_control|
        control = ::Inspec::Object::Control.new
        if (defined? control.desc).nil?
          control.descriptions[:default] = json_control['desc']
          control.descriptions[:rationale] = json_control['tags']['rationale']
          control.descriptions[:check] = json_control['tags']['check']
          control.descriptions[:fix] = json_control['tags']['fix']
        else
          control.desc = json_control['desc']
        end
        control.id     = json_control['id']
        control.title  = json_control['title']
        control.impact = get_impact(json_control['impact'])

        # json_control['tags'].each do |tag|
        #  control.add_tag(Inspec::Object::Tag.new(tag.key, tag.value)
        # end

        control.add_tag(::Inspec::Object::Tag.new('severity', json_control['tags']['severity']))
        control.add_tag(::Inspec::Object::Tag.new('gtitle', json_control['tags']['gtitle']))
        control.add_tag(::Inspec::Object::Tag.new('satisfies', json_control['tags']['satisfies'])) if json_control['tags']['satisfies']
        control.add_tag(::Inspec::Object::Tag.new('gid',      json_control['tags']['gid']))
        control.add_tag(::Inspec::Object::Tag.new('rid',      json_control['tags']['rid']))
        control.add_tag(::Inspec::Object::Tag.new('stig_id',  json_control['tags']['stig_id']))
        control.add_tag(::Inspec::Object::Tag.new('fix_id', json_control['tags']['fix_id']))
        control.add_tag(::Inspec::Object::Tag.new('cci', json_control['tags']['cci']))
        control.add_tag(::Inspec::Object::Tag.new('legacy', json_control['tags']['legacy'])) unless json_control['tags']['legacy'].blank?
        control.add_tag(::Inspec::Object::Tag.new('nist', json_control['tags']['nist']))
        control.add_tag(::Inspec::Object::Tag.new('cis_level', json_control['tags']['cis_level'])) unless json_control['tags']['cis_level'].blank?
        control.add_tag(::Inspec::Object::Tag.new('cis_controls', json_control['tags']['cis_controls'])) unless json_control['tags']['cis_controls'].blank?
        control.add_tag(::Inspec::Object::Tag.new('cis_rid', json_control['tags']['cis_rid'])) unless json_control['tags']['cis_rid'].blank?
        control.add_tag(::Inspec::Object::Tag.new('ref', json_control['tags']['ref'])) unless json_control['tags']['ref'].blank?
        control.add_tag(::Inspec::Object::Tag.new('false_negatives', json_control['tags']['false_negatives'])) unless json_control['tags']['false_positives'].blank?
        control.add_tag(::Inspec::Object::Tag.new('false_positives', json_control['tags']['false_positives'])) unless json_control['tags']['false_positives'].blank?
        control.add_tag(::Inspec::Object::Tag.new('documentable', json_control['tags']['documentable'])) unless json_control['tags']['documentable'].blank?
        control.add_tag(::Inspec::Object::Tag.new('mitigations', json_control['tags']['mitigations'])) unless json_control['tags']['mitigations'].blank?
        control.add_tag(::Inspec::Object::Tag.new('severity_override_guidance', json_control['tags']['severity_override_guidance'])) unless json_control['tags']['severity_override_guidance'].blank?
        control.add_tag(::Inspec::Object::Tag.new('security_override_guidance', json_control['tags']['security_override_guidance'])) unless json_control['tags']['security_override_guidance'].blank?
        control.add_tag(::Inspec::Object::Tag.new('potential_impacts', json_control['tags']['potential_impacts'])) unless json_control['tags']['potential_impacts'].blank?
        control.add_tag(::Inspec::Object::Tag.new('third_party_tools', json_control['tags']['third_party_tools'])) unless json_control['tags']['third_party_tools'].blank?
        control.add_tag(::Inspec::Object::Tag.new('mitigation_controls', json_control['tags']['mitigation_controls'])) unless json_control['tags']['mitigation_controls'].blank?
        control.add_tag(::Inspec::Object::Tag.new('responsibility', json_control['tags']['responsibility'])) unless json_control['tags']['responsibility'].blank?
        control.add_tag(::Inspec::Object::Tag.new('ia_controls', json_control['tags']['ia_controls'])) unless json_control['tags']['ia_controls'].blank?

        controls << control
      end
      controls
    end

    # @!method print_benchmark_info(info)
    # writes benchmark info to profile inspec.yml file
    #
    private_class_method def self.create_inspec_yml(directory, inspec_json)
      benchmark_info =
        "name: #{inspec_json['name']}\n" \
        "title: #{inspec_json['title']}\n" \
        "maintainer: #{inspec_json['maintainer']}\n" \
        "copyright: #{inspec_json['copyright']}\n" \
        "copyright_email: #{inspec_json['copyright_email']}\n" \
        "license: #{inspec_json['license']}\n" \
        "summary: #{inspec_json['summary']}\n" \
        "version: #{inspec_json['version']}\n"

      myfile = File.new("#{directory}/inspec.yml", 'w')
      myfile.puts benchmark_info
    end

    private_class_method def self.create_license(directory, inspec_json)
      license_content = ''
      if !inspec_json['license'].nil?
        begin
          response = Net::HTTP.get_response(URI(inspec_json['license']))
          if response.code == '200'
            license_content = response.body
          else
            license_content = inspec_json['license']
          end
        rescue StandardError => _e
          license_content = inspec_json['license']
        end
      end

      myfile = File.new("#{directory}/LICENSE", 'w')
      myfile.puts license_content
    end

    private_class_method def self.create_readme_md(directory, inspec_json)
      readme_contents =
        "\# #{inspec_json['title']}\n" \
        "#{inspec_json['summary']}\n" \
        "---\n" \
        "Name: #{inspec_json['name']}\n" \
        "Author: #{inspec_json['maintainer']}\n" \
        "Status: #{inspec_json['status']}\n" \
        "Copyright: #{inspec_json['copyright']}\n" \
        "Copyright Email: #{inspec_json['copyright_email']}\n" \
        "Version: #{inspec_json['version']}\n" \
        "#{inspec_json['plaintext']}\n" \
        "Reference: #{inspec_json['reference_href']}\n" \
        "Reference by: #{inspec_json['reference_publisher']}\n" \
        "Reference source: #{inspec_json['reference_source']}\n"

      myfile = File.new("#{directory}/README.md", 'w')
      myfile.puts readme_contents
    end

    private_class_method def self.unpack_profile(directory, controls, separated, output_format)
      FileUtils.rm_rf(directory) if Dir.exist?(directory)
      Dir.mkdir directory unless Dir.exist?(directory)
      Dir.mkdir "#{directory}/controls" unless Dir.exist?("#{directory}/controls")
      Dir.mkdir "#{directory}/libraries" unless Dir.exist?("#{directory}/libraries")
      if separated
        if output_format == 'ruby'
          controls.each do |control|
            file_name = control.id.to_s
            myfile = File.new("#{directory}/controls/#{file_name}.rb", 'w')
            myfile.puts "# encoding: UTF-8\n\n"
            myfile.puts "#{wrap(control.to_ruby, WIDTH)}\n"
            myfile.close
          end
        else
          controls.each do |control|
            file_name = control.id.to_s
            myfile = File.new("#{directory}/controls/#{file_name}.rb", 'w')
            PP.pp(control.to_hash, myfile)
            myfile.close
          end
        end
      else
        myfile = File.new("#{directory}/controls/controls.rb", 'w')
        if output_format == 'ruby'
          controls.each do |control|
            myfile.puts "# encoding: UTF-8\n\n"
            myfile.puts "#{wrap(control.to_ruby.gsub('"', "\'"), WIDTH)}\n"
          end
        else
          controls.each do |control|
            if (defined? control.desc).nil?
              control.descriptions[:default].strip!
            else
              control.desc.strip!
            end

            PP.pp(control.to_hash, myfile)
          end
        end
        myfile.close
      end
      config_store = ::RuboCop::ConfigStore.new
      config_store.options_config = File.join(File.dirname(__FILE__), '../data/rubocop.yml')
      rubocop = ::RuboCop::Runner.new({ auto_correct: true }, config_store)
      rubocop.run([directory])
    end
  end
end
