# typed: false
# frozen_string_literal: true
require "formula"
require "erb"
require "ostruct"
require "cli/parser"
require "completions"
module Homebrew
  extend T::Sig
  module_function
  SOURCE_PATH = (HOMEBREW_LIBRARY_PATH/"manpages").freeze
  TARGET_MAN_PATH = (HOMEBREW_REPOSITORY/"manpages").freeze
  TARGET_DOC_PATH = (HOMEBREW_REPOSITORY/"docs").freeze
  sig { returns(CLI::Parser) }
  def generate_man_completions_args
    Homebrew::CLI::Parser.new do
      description <<~EOS
        Generate Homebrew's manpages and shell completions.
      EOS
      switch "--fail-if-not-changed",
             description: "Return a failing status code if no changes are detected in the manpage outputs. "\
                          "This can be used to notify CI when the manpages are out of date. Additionally, "\
                          "the date used in new manpages will match those in the existing manpages (to allow "\
                          "comparison without factoring in the date)."
      named_args :none
    end
  end
  def generate_man_completions
    args = generate_man_completions_args.parse
    Commands.rebuild_internal_commands_completion_list
    regenerate_man_pages(preserve_date: args.fail_if_not_changed?, quiet: args.quiet?)
    Completions.update_shell_completions!
    diff = system_command "git", args: [
      "-C", HOMEBREW_REPOSITORY, "diff", "--exit-code", "docs/Manpage.md", "manpages", "completions"
    ]
    return unless diff.status.success?
    puts "No changes to manpage or completions output detected."
    Homebrew.failed = true if args.fail_if_not_changed?
  end
  def regenerate_man_pages(preserve_date:, quiet:)
    Homebrew.install_bundler_gems!
    markup = build_man_page(quiet: quiet)
    convert_man_page(markup, TARGET_DOC_PATH/"Manpage.md", preserve_date: preserve_date)
    markup = I18n.transliterate(markup, locale: :en)
    convert_man_page(markup, TARGET_MAN_PATH/"brew.1", preserve_date: preserve_date)
  end
  def build_man_page(quiet:)
    template = (SOURCE_PATH/"brew.1.md.erb").read
    variables = OpenStruct.new
    variables[:commands] = generate_cmd_manpages(Commands.internal_commands_paths)
    variables[:developer_commands] = generate_cmd_manpages(Commands.internal_developer_commands_paths)
    variables[:official_external_commands] =
      generate_cmd_manpages(Commands.official_external_commands_paths(quiet: quiet))
    variables[:global_cask_options] = global_cask_options_manpage
    variables[:global_options] = global_options_manpage
    variables[:environment_variables] = env_vars_manpage
    readme = HOMEBREW_REPOSITORY/"README.md"
    variables[:lead] =
      readme.read[/(Homebrew's \[Project Leader.*\.)/, 1]
            .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
    variables[:plc] =
      readme.read[/(Homebrew's \[Project Leadership Committee.*\.)/, 1]
            .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
    variables[:tsc] =
      readme.read[/(Homebrew's \[Technical Steering Committee.*\.)/, 1]
            .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
    variables[:linux] =
      readme.read[/(Homebrew's Linux maintainers .*\.)/, 1]
            .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
    variables[:maintainers] =
      readme.read[/(Homebrew's other current maintainers .*\.)/, 1]
            .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
    variables[:alumni] =
      readme.read[/(Former maintainers .*\.)/, 1]
            .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
    ERB.new(template, trim_mode: ">").result(variables.instance_eval { binding })
  end
  def sort_key_for_path(path)
    # Options after regular commands (`~` comes after `z` in ASCII table).
    path.basename.to_s.sub(/\.(rb|sh)$/, "").sub(/^--/, "~~")
  end
  def convert_man_page(markup, target, preserve_date:)
    manual = target.basename(".1")
    organisation = "Homebrew"
    # Set the manpage date to the existing one if we're checking for changes.
    # This avoids the only change being e.g. a new date.
    date = if preserve_date && target.extname == ".1" && target.exist?
      /"(\d{1,2})" "([A-Z][a-z]+) (\d{4})" "#{organisation}" "#{manual}"/ =~ target.read
      Date.parse("#{Regexp.last_match(1)} #{Regexp.last_match(2)} #{Regexp.last_match(3)}")
    else
      Date.today
    end
    date = date.strftime("%Y-%m-%d")
    shared_args = %W[
      --pipe
      --organization=#{organisation}
      --manual=#{target.basename(".1")}
      --date=#{date}
    ]
    format_flag, format_desc = target_path_to_format(target)
    puts "Writing #{format_desc} to #{target}"
    Utils.popen(["ronn", format_flag] + shared_args, "rb+") do |ronn|
      ronn.write markup
      ronn.close_write
      ronn_output = ronn.read
      odie "Got no output from ronn!" if ronn_output.blank?
      case format_flag
      when "--markdown"
        ronn_output = ronn_output.gsub(%r{(.*?)}, "*`\\1`*")
                                 .gsub(/\n\n\n+/, "\n\n")
                                 .gsub(/^(- `[^`]+`):/, "\\1") # drop trailing colons from definition lists
                                 .gsub(/(?<=\n\n)([\[`].+):\n/, "\\1\n
") # replace colons with 
 on subcommands
      when "--roff"
        ronn_output = ronn_output.gsub(%r{(.*?)}, "\\fB\\1\\fR")
                                 .gsub(%r{(.*?)}, "\\fI\\1\\fR")
                                 .gsub(/(^\[?\\fB.+): /, "\\1\n    ")
      end
      target.atomic_write ronn_output
    end
  end
  def target_path_to_format(target)
    case target.basename
    when /\.md$/    then ["--markdown", "markdown"]
    when /\.\d$/    then ["--roff", "man page"]
    else
      odie "Failed to infer output format from '#{target.basename}'."
    end
  end
  def generate_cmd_manpages(cmd_paths)
    man_page_lines = []
    # preserve existing manpage order
    cmd_paths.sort_by(&method(:sort_key_for_path))
             .each do |cmd_path|
      cmd_man_page_lines = if (cmd_parser = CLI::Parser.from_cmd_path(cmd_path))
        next if cmd_parser.hide_from_man_page
        cmd_parser_manpage_lines(cmd_parser).join
      else
        cmd_comment_manpage_lines(cmd_path)
      end
      man_page_lines << cmd_man_page_lines
    end
    man_page_lines.compact.join("\n")
  end
  def cmd_parser_manpage_lines(cmd_parser)
    lines = [format_usage_banner(cmd_parser.usage_banner_text)]
    lines += cmd_parser.processed_options.map do |short, long, _, desc, hidden|
      next if hidden
      if long.present?
        next if Homebrew::CLI::Parser.global_options.include?([short, long, desc])
        next if Homebrew::CLI::Parser.global_cask_options.any? do |_, option, description:, **|
                  [long, "#{long}="].include?(option) && description == desc
                end
      end
      generate_option_doc(short, long, desc)
    end.compact
    lines
  end
  def cmd_comment_manpage_lines(cmd_path)
    comment_lines = cmd_path.read.lines.grep(/^#:/)
    return if comment_lines.empty?
    return if comment_lines.first.include?("@hide_from_man_page")
    lines = [format_usage_banner(comment_lines.first).chomp]
    comment_lines.slice(1..-1)
                 .each do |line|
      line = line.slice(4..-2)
      unless line
        lines.last << "\n"
        next
      end
      # Omit the common global_options documented separately in the man page.
      next if line.match?(/--(debug|help|quiet|verbose) /)
      # Format one option or a comma-separated pair of short and long options.
      lines << line.gsub(/^ +(-+[a-z-]+), (-+[a-z-]+) +/, "* `\\1`, `\\2`:\n  ")
                   .gsub(/^ +(-+[a-z-]+) +/, "* `\\1`:\n  ")
    end
    lines.last << "\n"
    lines
  end
  sig { returns(String) }
  def global_cask_options_manpage
    lines = ["These options are applicable to the `install`, `reinstall`, and `upgrade` " \
             "subcommands with the `--cask` flag.\n"]
    lines += Homebrew::CLI::Parser.global_cask_options.map do |_, long, description:, **|
      generate_option_doc(nil, long.chomp("="), description)
    end
    lines.join("\n")
  end
  sig { returns(String) }
  def global_options_manpage
    lines = ["These options are applicable across multiple subcommands.\n"]
    lines += Homebrew::CLI::Parser.global_options.map do |short, long, desc|
      generate_option_doc(short, long, desc)
    end
    lines.join("\n")
  end
  sig { returns(String) }
  def env_vars_manpage
    lines = Homebrew::EnvConfig::ENVS.flat_map do |env, hash|
      entry = "- `#{env}`:\n  
#{hash[:description]}\n"
      default = hash[:default_text]
      default ||= "`#{hash[:default]}`." if hash[:default]
      entry += "\n\n  *Default:* #{default}\n" if default
      entry
    end
    lines.join("\n")
  end
  def generate_option_doc(short, long, desc)
    comma = (short && long) ? ", " : ""
    <<~EOS
      * #{format_short_opt(short)}#{comma}#{format_long_opt(long)}:
        #{desc}
    EOS
  end
  def format_short_opt(opt)
    "`#{opt}`" unless opt.nil?
  end
  def format_long_opt(opt)
    "`#{opt}`" unless opt.nil?
  end
  def format_usage_banner(usage_banner)
    usage_banner&.sub(/^(#: *\* )?/, "### ")
  end
end