# frozen_string_literal: true

require_relative '../../puppet/network/format_handler'
require_relative '../../puppet/util/json'

Puppet::Network::FormatHandler.create_serialized_formats(:msgpack, :weight => 20, :mime => "application/x-msgpack", :required_methods => [:render_method, :intern_method], :intern_method => :from_data_hash) do
  confine :feature => :msgpack

  def intern(klass, text)
    data = MessagePack.unpack(text)
    return data if data.is_a?(klass)

    klass.from_data_hash(data)
  end

  def intern_multiple(klass, text)
    MessagePack.unpack(text).collect do |data|
      klass.from_data_hash(data)
    end
  end

  def render_multiple(instances)
    instances.to_msgpack
  end
end

Puppet::Network::FormatHandler.create_serialized_formats(:yaml) do
  def allowed_yaml_classes
    @allowed_yaml_classes ||= [
      Puppet::Node::Facts,
      Puppet::Node,
      Puppet::Transaction::Report,
      Puppet::Resource,
      Puppet::Resource::Catalog
    ]
  end

  def intern(klass, text)
    data = Puppet::Util::Yaml.safe_load(text, allowed_yaml_classes)
    data_to_instance(klass, data)
  rescue Puppet::Util::Yaml::YamlLoadError => e
    raise Puppet::Network::FormatHandler::FormatError, _("Serialized YAML did not contain a valid instance of %{klass}: %{message}") % { klass: klass, message: e.message }
  end

  def intern_multiple(klass, text)
    data = Puppet::Util::Yaml.safe_load(text, allowed_yaml_classes)
    unless data.respond_to?(:collect)
      raise Puppet::Network::FormatHandler::FormatError, _("Serialized YAML did not contain a collection of instances when calling intern_multiple")
    end

    data.collect do |datum|
      data_to_instance(klass, datum)
    end
  rescue Puppet::Util::Yaml::YamlLoadError => e
    raise Puppet::Network::FormatHandler::FormatError, _("Serialized YAML did not contain a valid instance of %{klass}: %{message}") % { klass: klass, message: e.message }
  end

  def data_to_instance(klass, data)
    return data if data.is_a?(klass)

    unless data.is_a? Hash
      raise Puppet::Network::FormatHandler::FormatError, _("Serialized YAML did not contain a valid instance of %{klass}") % { klass: klass }
    end

    klass.from_data_hash(data)
  end

  def render(instance)
    instance.to_yaml
  end

  # Yaml monkey-patches Array, so this works.
  def render_multiple(instances)
    instances.to_yaml
  end

  def supported?(klass)
    true
  end
end

Puppet::Network::FormatHandler.create(:s, :mime => "text/plain", :charset => Encoding::UTF_8, :extension => "txt")

# By default, to_binary is called to render and from_binary called to intern. Note unlike
# text-based formats (json, yaml, etc), we don't use to_data_hash for binary.
Puppet::Network::FormatHandler.create(:binary, :mime => "application/octet-stream", :weight => 1,
                                               :required_methods => [:render_method, :intern_method]) do
end

# PSON is deprecated
Puppet::Network::FormatHandler.create_serialized_formats(:pson, :weight => 10, :required_methods => [:render_method, :intern_method], :intern_method => :from_data_hash) do
  confine :feature => :pson

  def intern(klass, text)
    data_to_instance(klass, PSON.parse(text))
  end

  def intern_multiple(klass, text)
    PSON.parse(text).collect do |data|
      data_to_instance(klass, data)
    end
  end

  # PSON monkey-patches Array, so this works.
  def render_multiple(instances)
    instances.to_pson
  end

  # If they pass class information, we want to ignore it.
  # This is required for compatibility with Puppet 3.x
  def data_to_instance(klass, data)
    d = data['data'] if data.is_a?(Hash)
    if d
      data = d
    end
    return data if data.is_a?(klass)

    klass.from_data_hash(data)
  end
end

Puppet::Network::FormatHandler.create_serialized_formats(:json, :mime => 'application/json', :charset => Encoding::UTF_8, :weight => 15, :required_methods => [:render_method, :intern_method], :intern_method => :from_data_hash) do
  def intern(klass, text)
    data_to_instance(klass, Puppet::Util::Json.load(text))
  end

  def intern_multiple(klass, text)
    Puppet::Util::Json.load(text).collect do |data|
      data_to_instance(klass, data)
    end
  end

  def render_multiple(instances)
    Puppet::Util::Json.dump(instances)
  end

  # Unlike PSON, we do not need to unwrap the data envelope, because legacy 3.x agents
  # have never supported JSON
  def data_to_instance(klass, data)
    return data if data.is_a?(klass)

    klass.from_data_hash(data)
  end
end

# This is really only ever going to be used for Catalogs.
Puppet::Network::FormatHandler.create_serialized_formats(:dot, :required_methods => [:render_method])

Puppet::Network::FormatHandler.create(:console,
                                      :mime => 'text/x-console-text',
                                      :weight => 0) do
  def json
    @json ||= Puppet::Network::FormatHandler.format(:json)
  end

  def render(datum)
    return datum if datum.is_a?(String) || datum.is_a?(Numeric)

    # Simple hash to table
    if datum.is_a?(Hash) && datum.keys.all? { |x| x.is_a?(String) || x.is_a?(Numeric) }
      output = ''.dup
      column_a = datum.empty? ? 2 : datum.map { |k, _v| k.to_s.length }.max + 2
      datum.sort_by { |k, _v| k.to_s }.each do |key, value|
        output << key.to_s.ljust(column_a)
        output << json.render(value)
                      .chomp.gsub(/\n */) { |x| x + (' ' * column_a) }
        output << "\n"
      end
      return output
    end

    # Print one item per line for arrays
    if datum.is_a? Array
      output = ''.dup
      datum.each do |item|
        output << item.to_s
        output << "\n"
      end
      return output
    end

    # ...or pretty-print the inspect outcome.
    Puppet::Util::Json.dump(datum, :pretty => true, :quirks_mode => true)
  end

  def render_multiple(data)
    data.collect(&:render).join("\n")
  end
end

Puppet::Network::FormatHandler.create(:flat,
                                      :mime => 'text/x-flat-text',
                                      :weight => 0) do
  def flatten_hash(hash)
    hash.each_with_object({}) do |(k, v), h|
      case v
      when Hash
        flatten_hash(v).map do |h_k, h_v|
          h["#{k}.#{h_k}"] = h_v
        end
      when Array
        v.each_with_index do |el, i|
          if el.is_a? Hash
            flatten_hash(el).map do |el_k, el_v|
              h["#{k}.#{i}.#{el_k}"] = el_v
            end
          else
            h["#{k}.#{i}"] = el
          end
        end
      else
        h[k] = v
      end
    end
  end

  def flatten_array(array)
    a = {}
    array.each_with_index do |el, i|
      if el.is_a? Hash
        flatten_hash(el).map do |el_k, el_v|
          a["#{i}.#{el_k}"] = el_v
        end
      else
        a[i.to_s] = el
      end
    end
    a
  end

  def construct_output(data)
    output = ''.dup
    data.each do |key, value|
      output << "#{key}=#{value}"
      output << "\n"
    end
    output
  end

  def render(datum)
    return datum if datum.is_a?(String) || datum.is_a?(Numeric)

    # Simple hash
    case datum
    when Hash
      data = flatten_hash(datum)
      return construct_output(data)
    when Array
      data = flatten_array(datum)
      return construct_output(data)
    end
    Puppet::Util::Json.dump(datum, :pretty => true, :quirks_mode => true)
  end

  def render_multiple(data)
    data.collect(&:render).join("\n")
  end
end

Puppet::Network::FormatHandler.create(:rich_data_json, mime: 'application/vnd.puppet.rich+json', charset: Encoding::UTF_8, weight: 30) do
  def intern(klass, text)
    Puppet.override({ :rich_data => true }) do
      data_to_instance(klass, Puppet::Util::Json.load(text))
    end
  end

  def intern_multiple(klass, text)
    Puppet.override({ :rich_data => true }) do
      Puppet::Util::Json.load(text).collect do |data|
        data_to_instance(klass, data)
      end
    end
  end

  def render(instance)
    Puppet.override({ :rich_data => true }) do
      instance.to_json
    end
  end

  def render_multiple(instances)
    Puppet.override({ :rich_data => true }) do
      Puppet::Util::Json.dump(instances)
    end
  end

  def data_to_instance(klass, data)
    Puppet.override({ :rich_data => true }) do
      return data if data.is_a?(klass)

      klass.from_data_hash(data)
    end
  end

  def supported?(klass)
    klass == Puppet::Resource::Catalog &&
      Puppet.lookup(:current_environment).rich_data?
  end
end

Puppet::Network::FormatHandler.create_serialized_formats(:rich_data_msgpack, mime: "application/vnd.puppet.rich+msgpack", weight: 35) do
  confine :feature => :msgpack

  def intern(klass, text)
    Puppet.override(rich_data: true) do
      data = MessagePack.unpack(text)
      return data if data.is_a?(klass)

      klass.from_data_hash(data)
    end
  end

  def intern_multiple(klass, text)
    Puppet.override(rich_data: true) do
      MessagePack.unpack(text).collect do |data|
        klass.from_data_hash(data)
      end
    end
  end

  def render_multiple(instances)
    Puppet.override(rich_data: true) do
      instances.to_msgpack
    end
  end

  def render(instance)
    Puppet.override(rich_data: true) do
      instance.to_msgpack
    end
  end

  def supported?(klass)
    suitable? &&
      klass == Puppet::Resource::Catalog &&
      Puppet.lookup(:current_environment).rich_data?
  end
end
