# frozen_string_literal: true

require_relative '../../puppet/application'

class Formatter
  def initialize(width)
    @width = width
  end

  def wrap(txt, opts)
    return "" unless txt && !txt.empty?

    work = (opts[:scrub] ? scrub(txt) : txt)
    indent = opts[:indent] || 0
    textLen = @width - indent
    patt = Regexp.new("\\A(.{0,#{textLen}})[ \n]")
    prefix = " " * indent

    res = []

    while work.length > textLen
      if work =~ patt
        res << ::Regexp.last_match(1)
        work.slice!(0, ::Regexp.last_match(0).length)
      else
        res << work.slice!(0, textLen)
      end
    end
    res << work if work.length.nonzero?
    prefix + res.join("\n#{prefix}")
  end

  def header(txt, sep = "-")
    "\n#{txt}\n" + sep * txt.size
  end

  private

  def scrub(text)
    # For text with no carriage returns, there's nothing to do.
    return text if text !~ /\n/

    # If we can match an indentation, then just remove that same level of
    # indent from every line.
    if text =~ /^(\s+)/
      indent = ::Regexp.last_match(1)
      text.gsub(/^#{indent}/, '')
    else
      text
    end
  end
end

class TypeDoc
  def initialize
    @format = Formatter.new(76)
    @types = {}
    Puppet::Type.loadall
    Puppet::Type.eachtype { |type|
      next if type.name == :component

      @types[type.name] = type
    }
  end

  def list_types
    puts "These are the types known to puppet:\n"
    @types.keys.sort_by(&:to_s).each do |name|
      type = @types[name]
      s = type.doc.gsub(/\s+/, " ")
      if s.empty?
        s = ".. no documentation .."
      else
        n = s.index(".") || s.length
        if n > 45
          s = s[0, 45] + " ..."
        else
          s = s[0, n]
        end
      end
      printf "%-15s - %s\n", name, s
    end
  end

  def format_type(name, opts)
    name = name.to_sym
    unless @types.has_key?(name)
      puts "Unknown type #{name}"
      return
    end
    type = @types[name]
    puts @format.header(name.to_s, "=")
    puts @format.wrap(type.doc, :indent => 0, :scrub => true) + "\n\n"

    puts @format.header("Parameters")
    if opts[:parameters]
      format_attrs(type, [:property, :param])
    else
      list_attrs(type, [:property, :param])
    end

    if opts[:meta]
      puts @format.header("Meta Parameters")
      if opts[:parameters]
        format_attrs(type, [:meta])
      else
        list_attrs(type, [:meta])
      end
    end

    if type.providers.size > 0
      puts @format.header("Providers")
      if opts[:providers]
        format_providers(type)
      else
        list_providers(type)
      end
    end
  end

  # List details about attributes
  def format_attrs(type, attrs)
    docs = {}
    type.allattrs.each do |name|
      kind = type.attrtype(name)
      docs[name] = type.attrclass(name).doc if attrs.include?(kind) && name != :provider
    end

    docs.sort { |a, b|
      a[0].to_s <=> b[0].to_s
    }.each { |name, doc|
      print "\n- **#{name}**"
      if type.key_attributes.include?(name) and name != :name
        puts " (*namevar*)"
      else
        puts ""
      end
      puts @format.wrap(doc, :indent => 4, :scrub => true)
    }
  end

  # List the names of attributes
  def list_attrs(type, attrs)
    params = []
    type.allattrs.each do |name|
      kind = type.attrtype(name)
      params << name.to_s if attrs.include?(kind) && name != :provider
    end
    puts @format.wrap(params.sort.join(", "), :indent => 4)
  end

  def format_providers(type)
    type.providers.sort_by(&:to_s).each { |prov|
      puts "\n- **#{prov}**"
      puts @format.wrap(type.provider(prov).doc, :indent => 4, :scrub => true)
    }
  end

  def list_providers(type)
    list = type.providers.sort_by(&:to_s).join(", ")
    puts @format.wrap(list, :indent => 4)
  end
end

class Puppet::Application::Describe < Puppet::Application
  banner "puppet describe [options] [type]"

  option("--short", "-s", "Only list parameters without detail") do |_arg|
    options[:parameters] = false
  end

  option("--providers", "-p")
  option("--list", "-l")
  option("--meta", "-m")

  def summary
    _("Display help about resource types available to OpenVox")
  end

  def help
    <<~HELP

      puppet-describe(8) -- #{summary}
      ========

      SYNOPSIS
      --------
      Prints help about Puppet resource types, providers, and metaparameters
      installed on an OpenVox node.


      USAGE
      -----
      puppet describe [-h|--help] [-s|--short] [-p|--providers] [-l|--list] [-m|--meta]


      OPTIONS
      -------
      * --help:
        Print this help text

      * --providers:
        Describe providers in detail for each type

      * --list:
        List all types

      * --meta:
        List all metaparameters

      * --short:
        List only parameters without detail


      EXAMPLE
      -------
          $ puppet describe --list
          $ puppet describe file --providers
          $ puppet describe user -s -m


      AUTHOR
      ------
      David Lutterkort


      COPYRIGHT
      ---------
      Copyright (c) 2011 Puppet Inc.
      Copyright (c) 2024 Vox Pupuli
      Licensed under the Apache 2.0 License

    HELP
  end

  def preinit
    options[:parameters] = true
  end

  def main
    doc = TypeDoc.new

    if options[:list]
      doc.list_types
    else
      options[:types].each { |name| doc.format_type(name, options) }
    end
  end

  def setup
    options[:types] = command_line.args.dup
    handle_help(nil) unless options[:list] || options[:types].size > 0
    $stderr.puts "Warning: ignoring types when listing all types" if options[:list] && options[:types].size > 0
  end
end
