# frozen_string_literal: true

require_relative '../../puppet'
require_relative '../../puppet/pops'
require_relative '../../puppet/pops/evaluator/json_strict_literal_evaluator'

class Puppet::InfoService::ClassInformationService
  def initialize
    @file_to_result = {}
    @parser = Puppet::Pops::Parser::EvaluatingParser.new()
  end

  def classes_per_environment(env_file_hash)
    # In this version of puppet there is only one way to parse manifests, as feature switches per environment
    # are added or removed, this logic needs to change to compute the result per environment with the correct
    # feature flags in effect.

    unless env_file_hash.is_a?(Hash)
      raise ArgumentError, _('Given argument must be a Hash')
    end

    result = {}

    # for each environment
    #   for each file
    #     if file already processed, use last result or error
    #
    env_file_hash.each do |env, files|
      env_result = result[env] = {}
      files.each do |f|
        env_result[f] = result_of(f)
      end
    end
    result
  end

  private

  def type_parser
    Puppet::Pops::Types::TypeParser.singleton
  end

  def literal_evaluator
    @@literal_evaluator ||= Puppet::Pops::Evaluator::JsonStrictLiteralEvaluator.new
  end

  def result_of(f)
    entry = @file_to_result[f]
    if entry.nil?
      @file_to_result[f] = entry = parse_file(f)
    end
    entry
  end

  def parse_file(f)
    return { :error => _("The file %{f} does not exist") % { f: f } } unless Puppet::FileSystem.exist?(f)

    begin
      parse_result = @parser.parse_file(f)
      { :classes =>
        parse_result.definitions.select { |d| d.is_a?(Puppet::Pops::Model::HostClassDefinition) }.map do |d|
          { :name => d.name,
            :params => d.parameters.map { |p| extract_param(p) } }
        end }
    rescue StandardError => e
      { :error => e.message }
    end
  end

  def extract_param(p)
    extract_default(extract_type({ :name => p.name }, p), p)
  end

  def extract_type(structure, p)
    return structure if p.type_expr.nil?

    structure[:type] = typeexpr_to_string(p.name, p.type_expr)
    structure
  end

  def extract_default(structure, p)
    value_expr = p.value
    return structure if value_expr.nil?

    default_value = value_as_literal(value_expr)
    structure[:default_literal] = default_value unless default_value.nil?
    structure[:default_source] = extract_value_source(value_expr)
    structure
  end

  def typeexpr_to_string(name, type_expr)
    type_parser.interpret_any(type_expr, nil).to_s
  rescue Puppet::ParseError => e
    raise Puppet::Error, "The parameter '$#{name}' is invalid: #{e.message}", e.backtrace
  end

  def value_as_literal(value_expr)
    catch(:not_literal) do
      return literal_evaluator.literal(value_expr)
    end
    nil
  end

  # Extracts the source for the expression
  def extract_value_source(value_expr)
    value_expr.locator.extract_tree_text(value_expr)
  end
end
