require_relative '../puppet_check'

# executes diagnostics on data files
class DataParser
  require 'yaml'

  # checks yaml (.yaml/.yml)
  def self.yaml(files)
    files.each do |file|
      # check yaml syntax
      parsed = YAML.load_file(file)
    rescue StandardError => err
      PuppetCheck.files[:errors][file] = err.to_s.gsub("(#{file}): ", '').split("\n")
    else
      warnings = []

      # perform some rudimentary hiera checks if data exists and is hieradata
      warnings = hiera(parsed, file) if parsed && (File.basename(file) != 'hiera.yaml')

      next PuppetCheck.files[:warnings][file] = warnings unless warnings.empty?
      PuppetCheck.files[:clean].push(file.to_s)
    end
  end

  # checks eyaml (.eyaml/.eyml)
  def self.eyaml(files, public, private)
    require 'openssl'

    # keys specified?
    if public.nil? || private.nil?
      PuppetCheck.files[:ignored].concat(files)
      return warn 'Public X509 and/or Private RSA PKCS7 certs were not specified. EYAML checks will not be executed.'
    end

    # keys exist?
    unless File.file?(public) && File.file?(private)
      PuppetCheck.files[:ignored].concat(files)
      return warn 'Specified Public X509 and/or Private RSA PKCS7 certs do not exist. EYAML checks will not be executed.'
    end

    # setup decryption
    rsa = OpenSSL::PKey::RSA.new(File.read(private))
    x509 = OpenSSL::X509::Certificate.new(File.read(public))

    files.each do |file|
      # check encoded yaml syntax
      parsed = YAML.load_file(file)

      # extract encoded values
      # ENC[PKCS7]

      # decrypt the encoded yaml
      # decrypted = OpenSSL::PKCS7.new(File.read(file)).decrypt(rsa, x509)

      # check decoded eyaml syntax
      # decoded = YAML.safe_load(decrypted)

      # merge data hashes
      # parsed = merge(parsed, decoded)
    rescue StandardError => err
      PuppetCheck.files[:errors][file] = err.to_s.gsub("(#{file}): ", '').split("\n")
    else
      warnings = []

      # perform some rudimentary hiera checks if data exists and is hieradata
      warnings = hiera(parsed, file) if parsed

      next PuppetCheck.files[:warnings][file] = warnings unless warnings.empty?
      PuppetCheck.files[:clean].push(file.to_s)
    end
  end

  # metadata consts
  REQUIRED_KEYS = %w[name version author license summary source dependencies].freeze
  REQ_DEP_KEYS = %w[requirements dependencies].freeze
  DEPRECATED_KEYS = %w[types checksum].freeze
  TASK_INPUTS = %w[environment stdin powershell].freeze

  # checks json (.json)
  def self.json(files)
    require 'json'

    files.each do |file|
      # check json syntax
      parsed = JSON.parse(File.read(file))
    rescue JSON::ParserError => err
      PuppetCheck.files[:errors][file] = err.to_s.lines.first.strip.split("\n")
    else
      warnings = []

      # check metadata.json
      if File.basename(file) == 'metadata.json'
        # metadata-json-lint has issues and is essentially no longer maintained, so here is an improved and leaner version of it
        require 'rubygems/util/licenses'

        # check for errors
        errors = []

        # check for required keys
        REQUIRED_KEYS.each do |key|
          errors.push("Required field '#{key}' not found.") unless parsed.key?(key)
        end

        # check requirements and dependencies keys
        REQ_DEP_KEYS.each do |key|
          # skip if key is missing or value is an empty string, array, or hash
          next if !parsed.key?(key) || parsed[key].empty?

          # check that dependencies and requirements are an array of hashes
          next errors.push("Field '#{key}' is not an array of hashes.") unless (parsed[key].is_a? Array) && (parsed[key][0].is_a? Hash)

          # check dependencies and requirements values
          names = []
          parsed[key].each do |req_dep|
            # check for duplicate dependencies and requirements
            name = req_dep['name']
            next errors.push("Duplicate #{key} on #{name}.") if names.include?(name)
            names << name

            # warn and skip if key is missing
            next warnings.push("'#{req_dep['name']}' is missing a 'version_requirement' key.") if req_dep['version_requirement'].nil?

            # warn and skip if no upper bound
            next warnings.push("'#{req_dep['name']}' is missing an upper bound.") unless req_dep['version_requirement'].include?('<')

            # check for semantic versioning
            if key == 'dependencies' && req_dep['version_requirement'] !~ /\d+\.\d+\.\d+.*\d+\.\d+\.\d+/
              warnings.push("'#{req_dep['name']}' has non-semantic versioning in its 'version_requirement' key.")
            end
          end
        end

        # check for deprecated fields
        DEPRECATED_KEYS.each do |key|
          errors.push("Deprecated field '#{key}' found.") if parsed.key?(key)
        end

        # check for summary under 144 character
        errors.push('Summary exceeds 144 characters.') if parsed.key?('summary') && parsed['summary'].size > 144

        next PuppetCheck.files[:errors][file] = errors unless errors.empty?

        # check for warnings
        # check for operatingsystem_support hash array
        if parsed.key?('operatingsystem_support')
          # check if operatingsystem_support array is actually empty
          if !(parsed['operatingsystem_support'].is_a? Array) || parsed['operatingsystem_support'].empty? || (!parsed['operatingsystem_support'].empty? && !(parsed['operatingsystem_support'][0].is_a? Hash))
            warnings.push('Recommended field \'operatingsystem\' not found.')
            warnings.push('Recommended field \'operatingsystemrelease\' not found.')
          else
            # check for operatingsystem string
            if parsed['operatingsystem_support'][0].key?('operatingsystem')
              warnings.push('Field \'operatingsystem\' is not a string.') unless parsed['operatingsystem_support'][0]['operatingsystem'].is_a? String
            else
              warnings.push('Recommended field \'operatingsystem\' not found.')
            end

            # check for operatingsystemrelease string array
            if parsed['operatingsystem_support'][0].key?('operatingsystemrelease')
              warnings.push('Field \'operatingsystemrelease\' is not a string array.') unless parsed['operatingsystem_support'][0]['operatingsystemrelease'][0].is_a? String
            else
              warnings.push('Recommended field \'operatingsystemrelease\' not found.')
            end
          end
        else
          warnings.push('Recommended field \'operatingsystem_support\' not found.')
        end

        # check for spdx license
        if parsed.key?('license') && !Gem::Licenses.match?(parsed['license']) && parsed['license'] !~ /[pP]roprietary/
          warnings.push("License identifier '#{parsed['license']}' is not in the SPDX list: http://spdx.org/licenses/")
        end
      # assume this is task metadata if it has this key
      elsif parsed.key?('description')
        # check that description is a string
        warnings.push('description value is not a String') unless parsed['description'].is_a?(String)
        # check that input_method is one of three possible values
        if parsed.key?('input_method')
          if parsed['input_method'].is_a?(String)
            warnings.push('input_method value is not one of environment, stdin, or powershell') unless TASK_INPUTS.include?(parsed['input_method'])
          else
            warnings.push('input_method value is not a String')
          end
        end
        # check that parameters is a hash
        if parsed.key?('parameters') && !parsed['parameters'].is_a?(Hash)
          warnings.push('parameters value is not a Hash')
        end
        # check that puppet_task_version is an integer
        if parsed.key?('puppet_task_version') && !parsed['puppet_task_version'].is_a?(Integer)
          warnings.push('puppet_task_version value is not an Integer')
        end
        # check that supports_noop is a boolean
        if parsed.key?('supports_noop') && !(parsed['supports_noop'].is_a?(TrueClass) || parsed['supports_noop'].is_a?(FalseClass))
          warnings.push('supports_noop value is not a Boolean')
        end
      # assume this is hieradata and ensure it is non-empty
      elsif parsed
        # perform some rudimentary hiera checks if data exists
        warnings = hiera(parsed, file)
      end
      next PuppetCheck.files[:warnings][file] = warnings unless warnings.empty?
      PuppetCheck.files[:clean].push(file.to_s)
    end
  end

  # checks hieradata
  private_class_method def self.hiera(data, file)
    warnings = []

    # disregard nil/undef value data check if default values (common)
    unless /^common/.match?(file)
      data.each do |key, value|
        # check for nil values in the data (nil keys are fine)
        if (value.is_a?(Hash) && value.values.any?(&:nil?)) || value.nil?
          warnings.push("Value(s) missing in key '#{key}'.")
        end
      end
    end

    # check that '---' does not show up more than once in the hieradata
    warnings.push('The string --- appears more than once in this data and Hiera may fail to parse it correctly.') if File.read(file).scan('---').count >= 2

    warnings
  end
end
