lib/gem_hadar.rb



require 'rubygems'
require 'rbconfig'
if defined?(::RbConfig)
  include ::RbConfig
else
  include ::Config
end
require 'rake'
require 'net/http'
require 'uri'
require 'tins/xt'
require 'tins/secure_write'
require 'rake/clean'
require 'rake/testtask'
require 'dslkit/polite'
require 'set'
require 'pathname'
require 'ollama'
require 'term/ansicolor'
require_maybe 'yard'
require_maybe 'simplecov'
require_maybe 'rubygems/package_task'
require_maybe 'rcov/rcovtask'
require_maybe 'rspec/core/rake_task'

# A brief description of the GemHadar class.
#
# The GemHadar class serves as the primary configuration and task management
# framework for Ruby gem projects. It provides a DSL for defining gem metadata,
# dependencies, and Rake tasks, while also offering integration with various
# tools such as GitHub, SimpleCov, YARD, and Ollama for automating common
# development workflows.
#
# @example Configuring a gem using the GemHadar DSL
#   GemHadar do
#     name        'my_gem'
#     version     '1.0.0'
#     author      'John Doe'
#     email       'john@example.com'
#     homepage    'https://github.com/example/my_gem'
#     summary     'A brief description'
#     description 'A longer description of the gem'
#     test_dir    'spec'
#   end
#
# @example Creating a Rake task for building and packaging the gem
#   GemHadar do
#     name 'my_gem'
#     # ... other configuration ...
#     build_task
#   end
#
# @example Setting up version bumping with AI assistance
#   GemHadar do
#     name 'my_gem'
#     # ... other configuration ...
#     version_bump_task
#   end
class GemHadar
end
require 'gem_hadar/version'
require 'gem_hadar/utils'
require 'gem_hadar/warn'
require 'gem_hadar/setup'
require 'gem_hadar/template_compiler'
require 'gem_hadar/github'
require 'gem_hadar/prompt_template'

class GemHadar
  include Term::ANSIColor
  include GemHadar::Utils
  include GemHadar::PromptTemplate
  include GemHadar::Warn

  if defined?(::RbConfig)
    include ::RbConfig
  else
    include ::Config
  end
  include Rake::DSL
  extend DSLKit::DSLAccessor
  include Tins::SecureWrite

  # The initialize method sets up the GemHadar instance by initializing
  # dependency arrays and evaluating a configuration block.
  #
  # @param block [Proc] optional configuration block to set gem properties and settings
  #
  # @return [GemHadar] the initialized GemHadar instance
  def initialize(&block)
    @dependencies = []
    @development_dependencies = []
    block and instance_eval(&block)
  end

  # The has_to_be_set method raises an error if a required gem configuration
  # attribute is not set.
  #
  # @param name [ String ] the name of the required attribute
  #
  # @raise [ ArgumentError ] if the specified attribute has not been set
  def has_to_be_set(name)
    fail "#{self.class}: #{name} has to be set for gem"
  end

  # The developing attribute accessor for configuring development mode.
  #
  # This method sets up a DSL accessor for the developing attribute, which
  # controls whether the gem is in development mode. When set to true, certain
  # behaviors such as skipping gem pushes are enabled as well as asserting the
  # validity of the homepage link.
  #
  # @return [ Boolean ] the current value of the developing flag
  dsl_accessor :developing, false

  # The name attribute accessor for configuring the gem's name.
  #
  # This method sets up a DSL accessor for the name attribute, which specifies
  # the identifier for the gem. It includes a validation step that raises an
  # ArgumentError if the name has not been set, ensuring that the gem
  # configuration contains a required name value.
  #
  # @return [ String ] the name of the gem
  #
  # @raise [ ArgumentError ] if the name attribute has not been set
  dsl_accessor :name do
    has_to_be_set :name
  end

  # The name_version method computes and returns the combined gem name and
  # version string.
  #
  # This method constructs a version identifier by joining the gem's name and
  # current version with a hyphen separator. It is typically used to generate
  # filenames or identifiers that incorporate both the gem name and its version
  # number for packaging, tagging, or display purposes.
  #
  # @return [ String ] the combined gem name and version in the format "name-version"
  dsl_accessor :name_version do
    [ name, version ] * '-'
  end

  # The module_type attribute accessor for configuring the type of Ruby construct to generate for version code.
  #
  # This method sets up a DSL accessor for the module_type attribute, which determines whether the generated code
  # structure for the version module should be a :module or :class. This controls the type of Ruby construct
  # created when generating code skeletons and version files. The value can be set to either:
  #
  # - :module (default) - Generates module-based structure
  # - :class - Generates class-based structure
  #
  # This is used in the generated version.rb file to create either:
  #
  #   module MyGem
  #     # ... version constants
  #   end
  #
  # or
  #
  #   class MyGem
  #     # ... version constants
  #   end
  #
  # @return [ Symbol ] the type of Ruby construct to generate (:module or :class)
  dsl_accessor :module_type, :module

  # The has_to_be_set method raises an error if a required gem configuration
  # attribute is not set.
  #
  # This method is used to validate that essential gem configuration attributes
  # have been provided. When called, it will raise an ArgumentError with a
  # descriptive message indicating which attribute is missing and needs to be
  # configured.
  #
  # @param name [ String ] the name of the required attribute
  #
  # @raise [ ArgumentError ] if the specified attribute has not been set
  dsl_accessor :author do
    has_to_be_set :author
  end

  # The email attribute accessor for configuring the gem's author email.
  #
  # This method sets up a DSL accessor for the email attribute, which specifies
  # the contact email address for the gem's author. It includes a
  # validation step that raises an ArgumentError if the email has not been
  # set, ensuring that the gem configuration contains this required
  # information.
  #
  # @return [ String ] the author's email address
  #
  # @raise [ ArgumentError ] if the email attribute has not been set
  dsl_accessor :email do
    has_to_be_set :email
  end

  # The homepage attribute accessor for configuring the gem's homepage URL.
  #
  # This method sets up a DSL accessor for the homepage attribute, which
  # specifies the URL of the gem's official repository or project page. It
  # includes a validation step that raises an ArgumentError if the homepage has
  # not been set, ensuring that the gem configuration contains this required
  # information. When the developing flag is false, it also validates that the
  # provided URL returns an HTTP OK status after following redirects.
  #
  # @return [ String ] the homepage URL of the gem
  #
  # @raise [ ArgumentError ] if the homepage attribute has not been set
  # @raise [ ArgumentError ] if the homepage URL is invalid and developing mode is disabled
  dsl_accessor :homepage do
    has_to_be_set :homepage
  end

  # The summary attribute accessor for configuring the gem's summary.
  #
  # This method sets up a DSL accessor for the summary attribute, which
  # specifies a brief description of what the gem does. It includes a
  # validation step that raises an ArgumentError if the summary has not been
  # set, ensuring that the gem configuration contains this required
  # information.
  #
  # @return [ String ] the summary of the gem
  #
  # @raise [ ArgumentError ] if the summary attribute has not been set
  dsl_accessor :summary do
    has_to_be_set :summary
  end

  # The description attribute accessor for configuring the gem's description.
  #
  # This method sets up a DSL accessor for the description attribute, which
  # specifies the detailed explanation of what the gem does. It includes a
  # validation step that raises an ArgumentError if the description has not
  # been set, ensuring that the gem configuration contains this required
  # information.
  #
  # @return [ String ] the description of the gem
  #
  # @raise [ ArgumentError ] if the description attribute has not been set
  dsl_accessor :description do has_to_be_set :description end

  # The require_paths attribute accessor for configuring the gem's require
  # paths.
  #
  # This method sets up a DSL accessor for the require_paths attribute, which
  # specifies the directories from which the gem's code can be loaded. It
  # provides a way to define the locations of the library files that will be
  # made available to users of the gem when it is required in Ruby programs.
  #
  # @return [ Set<String> ] a set of directory paths to be included in the load
  # path
  dsl_accessor :require_paths do Set['lib'] end

  # The require_path method manages the gem's require path configuration.
  #
  # This method provides functionality to set or retrieve the directory paths
  # from which the gem's code can be loaded. When called with a path argument,
  # it updates the require_paths attribute with that path and returns it.
  # When called without arguments, it returns the first path from the current
  # require_paths set.
  #
  # @param path [ String, nil ] the directory path to set as the require path;
  #                           if nil, returns the current first require path
  #
  # @return [ String ] the specified path when setting, or the first require
  # path when retrieving
  def require_path(path = nil)
    if path
      self.require_paths = Set[path]
      path
    else
      require_paths.first
    end
  end

  # The readme attribute accessor for configuring the gem's README file.
  #
  # This method sets up a DSL accessor for the readme attribute, which specifies
  # the path to the README file for the gem. It provides a way to define the
  # location of the README file that will be used in documentation and packaging
  # processes.
  #
  # @return [ String, nil ] the path to the README file or nil if not set
  dsl_accessor :readme

  # The title attribute accessor for configuring the gem's documentation title.
  #
  # This method sets up a DSL accessor for the title attribute, which specifies
  # the title to be used in the generated YARD documentation. It provides a way
  # to define a custom title that will be included in the documentation output,
  # making it easier to identify and reference the gem's documentation.
  #
  # @return [ String, nil ] the documentation title or nil if not set
  dsl_accessor :title

  # The ignore_files attribute accessor for configuring files to be ignored by
  # the gem.
  #
  # This method sets up a DSL accessor for the ignore_files attribute, which
  # specifies a set of file patterns that should be excluded from various gem
  # operations and processing tasks. It provides a way to define ignore rules
  # that apply broadly across the gem's functionality, including but not
  # limited to build processes, documentation generation, and version control
  # integration.
  #
  # @return [ Set<String> ] a set of file patterns to be ignored by the gem's operations
  dsl_accessor :ignore_files do Set[] end

  # The bindir attribute accessor for configuring the gem's binary directory.
  #
  # This method sets up a DSL accessor for the bindir attribute, which specifies
  # the directory where executable scripts (binaries) are installed when the gem
  # is packaged and installed. It provides a way to define the location of the
  # bin directory that will contain the gem's executable files.
  #
  # @return [ String, nil ] the path to the binary directory or nil if not set
  dsl_accessor :bindir

  # The executables attribute accessor for configuring the gem's executable
  # files.
  #
  # This method sets up a DSL accessor for the executables attribute, which
  # specifies the list of executable scripts that should be installed as part
  # of the gem. It provides a way to define one or more executable file names
  # that will be made available in the gem's bin directory when the gem is
  # installed.
  #
  # @return [ Set<String> ] a set of executable file names to be included with
  # the gem
  dsl_accessor :executables do Set[] end

  # The licenses attribute accessor for configuring the gem's license
  # information.
  #
  # This method sets up a DSL accessor for the licenses attribute, which
  # specifies the license(s) under which the gem is distributed. It provides a
  # way to define one or more licenses that apply to the gem, defaulting to an
  # empty Set if none are explicitly configured.
  #
  # @return [ Set<String> ] a set of license identifiers applied to the gem
  dsl_accessor :licenses do Set[] end

  # The test_dir attribute accessor for configuring the test directory.
  #
  # This method sets up a DSL accessor for the test_dir attribute, which specifies
  # the directory where test files are located. It provides a way to define the
  # location of the test directory that will be used by various testing tasks and
  # configurations within the gem project.
  #
  # @return [ String, nil ] the path to the test directory or nil if not set
  dsl_accessor :test_dir

  # The test_files attribute accessor for configuring the list of test files to
  # be included in the gem package.
  #
  # This method sets up a DSL accessor for the test_files attribute, which
  # specifies the files that should be included when running tests for the gem.
  # It provides a way to customize the test file discovery process, defaulting
  # to finding all Ruby files ending in _spec.rb within the spec directory and
  # its subdirectories.
  #
  # @return [ FileList ] a list of file paths to be included in test execution
  dsl_accessor :test_files do
    if test_dir
      FileList[File.join(test_dir, '**/*.rb')]
    else
      FileList.new
    end
  end

  # The spec_dir attribute accessor for configuring the RSpec specification
  # directory.
  #
  # This method sets up a DSL accessor for the spec_dir attribute, which
  # specifies the directory where RSpec test files are located. It provides a
  # way to customize the location of test specifications separate from the
  # default 'spec' directory, allowing for more flexible project structures.
  #
  # @return [ String, nil ] the path to the RSpec specification directory or
  # nil if not set
  dsl_accessor :spec_dir

  # The spec_pattern method configures the pattern used to locate RSpec test
  # files.
  #
  # This method sets up a DSL accessor for the spec_pattern attribute, which
  # defines the file pattern used to discover RSpec test files in the project.
  # It defaults to a standard pattern that looks for files ending in _spec.rb
  # within the spec directory and its subdirectories, but can be customized
  # through the configuration block.
  #
  # @return [ String ] the file pattern used to locate RSpec test files
  dsl_accessor :spec_pattern do
    if spec_dir
      "#{spec_dir}{,/*/**}/*_spec.rb"
    else
      'spec{,/*/**}/*_spec.rb'
    end
  end

  # The doc_files attribute accessor for configuring additional documentation
  # files.
  #
  # This method sets up a DSL accessor for the doc_files attribute, which
  # specifies additional files to be included in the YARD documentation
  # generation process. It defaults to an empty FileList and provides a way to
  # define extra documentation files that should be processed alongside the
  # standard library source files.
  #
  # @return [ FileList ] a list of file paths to be included in YARD
  # documentation
  dsl_accessor :doc_files do
    FileList[File.join('lib/**/*.rb')] + FileList[File.join('ext/**/*.c')]
  end

  # The yard_dir attribute accessor for configuring the output directory for
  # YARD documentation.
  #
  # This method sets up a DSL accessor for the yard_dir attribute, which
  # specifies the directory where YARD documentation will be generated. It
  # defaults to 'doc' and provides a way to customize the documentation output
  # location through the configuration block.
  #
  # @return [ String ] the path to the directory where YARD documentation will be stored
  dsl_accessor :yard_dir do
    'doc'
  end

  # The extensions attribute accessor for configuring project extensions.
  #
  # This method sets up a DSL accessor for the extensions attribute, which
  # specifies the list of extension configuration files (typically extconf.rb)
  # that should be compiled when building the gem. It defaults to finding all
  # extconf.rb files within the ext directory and its subdirectories, making it
  # easy to include native extensions in the gem package.
  #
  # @return [ FileList ] a list of file paths to extension configuration files
  #                      to be compiled during the build process
  dsl_accessor :extensions do FileList['ext/**/extconf.rb'] end

  # The make method retrieves the make command to be used for building
  # extensions.
  #
  # This method determines the appropriate make command to use when compiling
  # project extensions. It first checks for the MAKE environment variable and
  # returns its value if set. If the environment variable is not set, it
  # attempts to detect a suitable make command by testing for the existence of
  # 'gmake' and 'make' in the system PATH.
  #
  # @return [ String, nil ] the make command name or nil if none found
  dsl_accessor :make do
    ENV['MAKE'] || %w[gmake make].find { |c| system(c, '-v') }
  end

  # The files attribute accessor for configuring the list of files included in
  # the gem package.
  #
  # This method sets up a DSL accessor for the files attribute, which specifies
  # the complete set of files that should be included when building the gem
  # package. It defaults to retrieving the file list from Git using `git
  # ls-files` and provides a way to override this behavior through the
  # configuration block.
  #
  # @return [ Array<String> ] an array of file paths to be included in the gem package
  dsl_accessor :files do
    `git ls-files`.split("\n")
  end

  # The package_ignore_files attribute accessor for configuring files to be
  # ignored during gem packaging.
  #
  # This method sets up a DSL accessor for the package_ignore_files attribute,
  # which specifies file patterns that should be excluded from the gem package
  # when it is built. It defaults to an empty set and provides a way to define
  # ignore rules specific to the packaging process, separate from general
  # project ignore rules.
  #
  # @return [ Set<String> ] a set of file patterns to be ignored during gem packaging
  dsl_accessor :package_ignore_files do
    Set[]
  end

  # The path_name attribute accessor for configuring the gem's path name.
  #
  # This method sets up a DSL accessor for the path_name attribute, which
  # determines the raw gem name value used for generating file paths and module
  # names. It defaults to the value of the name attribute and is particularly
  # useful for creating consistent directory structures and file naming
  # conventions. This value is used internally by GemHadar to create the root
  # directory for the gem and generate a version.rb file in that location.
  #
  # @return [ String ] the path name derived from the gem's name
  dsl_accessor :path_name do name end

  # The path_module attribute accessor for configuring the Ruby module name.
  #
  # This method sets up a DSL accessor for the path_module attribute, which
  # determines the camelized version of the gem's name to be used as the Ruby
  # module or class name. It automatically converts the value of path_name
  # into CamelCase format, ensuring consistency with Ruby naming conventions
  # for module and class declarations.
  #
  # @return [ String ] the camelized module name derived from path_name
  dsl_accessor :path_module do path_name.camelize end

  # The version attribute accessor for configuring the gem's version.
  #
  # This method sets up a DSL accessor for the version attribute, which
  # specifies the version number of the gem. It includes logic to determine the
  # version from the VERSION file or an environment variable override, and will
  # raise an ArgumentError if the version has not been set and cannot be
  # determined.
  #
  # @return [ String ] the version of the gem
  #
  # @raise [ ArgumentError ] if the version attribute has not been set and
  #                           cannot be read from the VERSION file or ENV override
  dsl_accessor :version do
    v = ENV['VERSION'].full? and next v
    File.read('VERSION').chomp
  rescue Errno::ENOENT
    has_to_be_set :version
  end

  # The version_epilogue attribute accessor for configuring additional content
  # to be appended to the version file.
  #
  # This method sets up a DSL accessor for the version_epilogue attribute,
  # which specifies extra content to be included at the end of the generated
  # version file. This can be useful for adding custom comments, license
  # information, or other supplementary data to the version module or class.
  #
  # @return [ String, nil ] the epilogue content or nil if not set
  dsl_accessor :version_epilogue

  # The post_install_message attribute accessor for configuring a message to
  # display after gem installation.
  #
  # This method sets up a DSL accessor for the post_install_message attribute,
  # which specifies a message to be displayed to users after the gem is installed.
  # This can be useful for providing additional information, usage instructions,
  # or important warnings to users of the gem.
  #
  # @return [ String, nil ] the post-installation message or nil if not set
  dsl_accessor :post_install_message

  # The required_ruby_version attribute accessor for configuring the minimum
  # Ruby version requirement.
  #
  # This method sets up a DSL accessor for the required_ruby_version attribute,
  # which specifies the minimum version of Ruby that the gem requires to run.
  # It allows defining the Ruby version constraint that will be included in the
  # gem specification.
  #
  # @return [ String, nil ] the required Ruby version string or nil if not set
  dsl_accessor :required_ruby_version

  # A class that encapsulates Ruby Version Manager (RVM) configuration settings
  # for a gem project.
  #
  # This class is responsible for managing RVM-specific configuration such as
  # the Ruby version to use and the gemset name. It provides a structured way
  # to define and access these settings within the context of a GemHadar
  # configuration.
  #
  # @example Configuring RVM settings
  #   GemHadar do
  #     rvm do
  #       use '3.0.0'
  #       gemset 'my_gem_dev'
  #     end
  #   end
  class RvmConfig
    extend DSLKit::DSLAccessor
    include DSLKit::BlockSelf

    # The initialize method sets up the RvmConfig instance by evaluating the
    # provided block in the context of the object.
    #
    # @param block [ Proc ] the block to be evaluated for configuring the RVM settings
    #
    # @return [ GemHadar::RvmConfig ] the initialized RvmConfig instance
    def initialize(&block)
      @outer_scope = block_self(&block)
      instance_eval(&block)
    end

    # The use method retrieves or sets the Ruby version to be used with RVM.
    #
    # This method serves as an accessor for the Ruby version configuration
    # within the RVM (Ruby Version Manager) settings. When called without
    # arguments, it returns the configured Ruby version. When called with
    # an argument, it sets the Ruby version to be used.
    #
    # @return [ String ] the Ruby version string configured for RVM use
    # @see GemHadar::RvmConfig
    dsl_accessor :use do `rvm tools strings`.split(/\n/).full?(:last) || 'ruby' end

    # The gemset method retrieves or sets the RVM gemset name for the project.
    #
    # This method serves as an accessor for the RVM (Ruby Version Manager)
    # gemset configuration within the nested RvmConfig class. When called
    # without arguments,
    # it returns the configured gemset name, which defaults to the gem's name.
    # When called with an argument, it sets the gemset name to be used with RVM.
    #
    # @return [ String ] the RVM gemset name configured for the project
    # @see GemHadar::RvmConfig#use
    # @see GemHadar::RvmConfig
    dsl_accessor :gemset do @outer_scope.name end
  end

  # The rvm method configures RVM (Ruby Version Manager) settings for the gem
  # project.
  #
  # This method initializes and returns an RvmConfig object that holds RVM-specific
  # configuration such as the Ruby version to use and the gemset name.
  # If a block is provided, it configures the RvmConfig object with the given
  # settings. If no block is provided and no existing RvmConfig object exists,
  # it creates a new one with default settings.
  #
  # @param block [ Proc ] optional block to configure RVM settings
  #
  # @return [ GemHadar::RvmConfig ] the RVM configuration object
  def rvm(&block)
    if block
      @rvm = RvmConfig.new(&block)
    elsif !@rvm
      @rvm = RvmConfig.new { }
    end
    @rvm
  end

  # The default_task_dependencies method manages the list of dependencies for
  # the default Rake task.
  #
  # This method sets up a DSL accessor for the default_task_dependencies
  # attribute, which specifies the sequence of Rake tasks that must be executed
  # when running the default task. These dependencies typically include
  # generating the gem specification and running tests to ensure a consistent
  # starting point for development workflows.
  #
  # @return [ Array<Symbol, String> ] an array of task names that are required
  #                                   as dependencies for the default task execution
  dsl_accessor :default_task_dependencies, [ :gemspec, :test ]

  # The default_task method defines the default Rake task for the gem project.
  #
  # This method sets up a Rake task named :default that depends on the tasks
  # specified in the default_task_dependencies accessor. It provides a convenient
  # way to run the most common or essential tasks for the project with a single
  # command.
  def default_task
    desc 'Default task'
    task :default => default_task_dependencies
  end

  # The build_task_dependencies method manages the list of dependencies for the
  # build task.
  #
  # This method sets up a DSL accessor for the build_task_dependencies
  # attribute, which specifies the sequence of Rake tasks that must be executed
  # when running the build task. These dependencies typically include cleaning
  # previous builds, generating the gem specification, packaging the gem, and
  # creating a version tag.
  #
  # @return [ Array<Symbol, String> ] an array of task names that are required
  #                                   as dependencies for the build task execution
  dsl_accessor :build_task_dependencies, [ :clobber, :gemspec, :package, :'version:tag' ]

  # The build_task method defines a Rake task that orchestrates the complete
  # build process for packaging the gem.
  #
  # This method sets up a :build task that depends on the tasks specified in
  # the build_task_dependencies accessor. It provides a convenient way to
  # execute all necessary steps for building packages for a release with a
  # single command.
  def build_task
    desc 'Build task (builds all packages for a release)'
    task :build => build_task_dependencies
  end

  # The install_library method sets up a Rake task for installing the library
  # or executable into site_ruby directories.
  #
  # This method configures an :install task that depends on the
  # :prepare_install task and executes the provided block. It stores the block
  # in an instance variable to be called later when the task is executed.
  #
  # @param block [ Proc ] the block containing the installation logic
  def install_library(&block)
    @install_library_block = -> do
      desc 'Install executable/library into site_ruby directories'
      task :install => :prepare_install, &block
    end
  end

  # The clean method manages the CLEAN file list for Rake tasks.
  #
  # When called without arguments, it returns the current CLEAN file list.
  # When called with arguments, it adds the specified files to the CLEAN list.
  #
  # @param args [ Array<String> ] optional list of files to add to the CLEAN list
  #
  # @return [ FileList, nil ] the CLEAN file list when no arguments provided,
  #                           nil otherwise
  def clean(*args)
    if args.empty?
      CLEAN
    else
      CLEAN.include(*args)
    end
  end

  # The clobber method manages the CLOBBER file list for Rake tasks.
  #
  # When called without arguments, it returns the current CLOBBER file list.
  # When called with arguments, it adds the specified files to the CLOBBER list.
  #
  # @param args [ Array<String> ] optional list of files to add to the CLOBBER list
  #
  # @return [ FileList, nil ] the CLOBBER file list when no arguments provided,
  #                           nil otherwise
  def clobber(*args)
    if args.empty?
      CLOBBER
    else
      CLOBBER.include(*args)
    end
  end

  # The ignore method manages the list of files to be ignored by the gem.
  #
  # When called without arguments, it returns the current set of ignored files.
  # When called with arguments, it adds the specified files to the ignore list.
  #
  # @param args [ Array<String> ] optional list of file patterns to add to the ignore list
  #
  # @return [ Set<String>, nil ] the set of ignored files when no arguments provided,
  #                           nil otherwise
  def ignore(*args)
    if args.empty?
      ignore_files
    else
      args.each { |a| ignore_files << a }
    end
  end

  # The package_ignore method manages the list of files to be ignored during
  # gem packaging.
  #
  # When called without arguments, it returns the current set of package ignore
  # files. When called with arguments, it adds the specified file patterns to
  # the package ignore list.
  #
  # @param args [ Array<String> ] optional list of file patterns to add to the package ignore list
  #
  # @return [ Set<String>, nil ] the set of package ignore files when no arguments provided,
  #                           nil otherwise
  def package_ignore(*args)
    if args.empty?
      package_ignore_files
    else
      args.each { |a| package_ignore_files << a }
    end
  end

  # The dependency method adds a new runtime dependency to the gem.
  #
  # @param args [ Array ] the arguments defining the dependency
  def dependency(*args)
    @dependencies << args
  end

  # The development_dependency method adds a new development-time dependency to
  # the gem.
  #
  # @param args [ Array ] the arguments defining the development dependency
  def development_dependency(*args)
    @development_dependencies << args
  end

  # The gems_install_task method defines a Rake task for installing all gem
  # dependencies specified in the Gemfile.
  #
  # This method sets up a :gems:install task that executes a block to install
  # gems. If no block is provided, it defaults to running 'bundle install'.
  #
  # @param block [ Proc ] optional block containing the installation command
  def gems_install_task(&block)
    block ||= proc {  sh 'bundle install' }
    desc 'Install all gems from the Gemfile'
    namespace :gems do
      task :install => :gemspec , &block
    end
  end

  # The version_task method defines a Rake task that generates a version file
  # for the gem.
  #
  # This method creates a task named :version that writes version information
  # to a Ruby file in the lib directory. The generated file contains constants
  # for the version and its components, as well as an optional epilogue
  # section. The task ensures the target directory exists and uses secure file
  # writing to prevent permission issues.
  def version_task
    desc m = "Writing version information for #{name}-#{version}"
    task :version do
      puts m
      mkdir_p dir = File.join('lib', path_name)
      secure_write(File.join(dir, 'version.rb')) do |v|
        v.puts <<~EOT
          #{module_type} #{path_module}
            # #{path_module} version
            VERSION         = '#{version}'
            VERSION_ARRAY   = VERSION.split('.').map(&:to_i) # :nodoc:
            VERSION_MAJOR   = VERSION_ARRAY[0] # :nodoc:
            VERSION_MINOR   = VERSION_ARRAY[1] # :nodoc:
            VERSION_BUILD   = VERSION_ARRAY[2] # :nodoc:
          end
        EOT
        version_epilogue.full? { |ve| v.puts ve }
      end
    end
  end

  # The version_show_task method defines a Rake task that displays the current
  # version of the gem.
  #
  # This method creates a :version:show task under the Rake namespace that
  # reads the version from the generated version file in the lib directory and
  # compares it with the version specified in the GemHadar configuration. It
  # then outputs a message indicating whether the versions match or not.
  def version_show_task
    namespace :version do
      desc "Displaying the current version"
      task :show do
        require path_name
        dir = File.join('lib', path_name)
        version_file = File.join(dir, 'version.rb')
        m = Module.new
        m.instance_eval File.read(version_file)
        version_rb   = m.const_get(
          [ path_module, 'VERSION' ] * '::'
        )
        equal        = version == version_rb ? '==' : '!='
        puts "version.rb=#{version_rb} #{equal} VERSION=#{version}"
      end
    end
  end

  # The version_log_diff method generates a git log output containing patch
  # differences between two specified versions.
  #
  # This method retrieves the commit history between a starting version and an
  # ending version, including detailed changes (patch format) for each commit.
  # It supports comparing against the current HEAD or specific version tags,
  # and automatically determines the appropriate previous version when only a
  # target version is provided.
  #
  # @param to_version [ String ] the ending version tag or 'HEAD' to compare up to the latest commit
  # @param from_version [ String, nil ] the starting version tag; if nil, it defaults based on to_version
  #
  # @raise [ RuntimeError ] if the specified version tags are not found in the repository
  #
  # @return [ String ] the git log output in patch format showing changes between the two versions
  def version_log_diff(to_version: 'HEAD', from_version: nil)
    if to_version == 'HEAD'
      if from_version.blank?
        from_version = versions.last
      else
        unless versions.find { |v| v == from_version }
          fail "Could not find #{from_version.inspect}."
        end
      end
      `git log -p #{version_tag(from_version)}..HEAD`
    else
      unless versions.find { |v| v == to_version }
        fail "Could not find #{to_version.inspect}."
      end
      if from_version.blank?
        from_version = versions.each_cons(2).find do |previous_version, v|
          if v == to_version
            break previous_version
          end
        end
        unless from_version
          return `git log -p #{version_tag(to_version)}`
        end
      else
        unless versions.find { |v| v == from_version }
          fail "Could not find #{from_version.inspect}."
        end
      end
      `git log -p #{version_tag(from_version)}..#{version_tag(to_version)}`
    end
  end

  # The version_diff_task method defines Rake tasks for listing and displaying
  # git version differences.
  #
  # This method sets up two subtasks under the :version namespace:
  #
  # - A :list task that fetches all git tags, ensures the operation succeeds,
  # and outputs the sorted list of versions.
  # - A :diff task that calculates the version range, displays a colored diff
  # between the versions, and shows the changes.
  def version_diff_task
    namespace :version do
      desc "List all versions in order"
      task :list do
        system 'git fetch --tags'
        $?.success? or exit $?.exitstatus
        puts versions
      end

      desc "Displaying the diff from env var VERSION to the next version or HEAD"
      task :diff do
        start_version, end_version = determine_version_range
        puts color(172) { "Showing diff from version %s to %s:" % [ start_version, end_version ] }
        puts `git diff --color=always #{start_version}..#{end_version}`
      end
    end
  end

  # The gem_hadar_update_task method defines a Rake task that updates the
  # gem_hadar dependency version in the gemspec file.
  #
  # This method creates a :gem_hadar:update task under the Rake namespace that
  # prompts the user to specify a new gem_hadar version.
  # It then reads the existing gemspec file, modifies the version constraint
  # for the gem_hadar dependency, and writes the updated content back to the
  # file. If the specified version is already present in the gemspec, it skips
  # the update and notifies the user.
  def gem_hadar_update_task
    namespace :gem_hadar do
      desc 'Update gem_hadar to a different version'
      task :update do
        answer = ask?("Which gem_hadar version? ", /^((?:\d+.){2}(?:\d+))$/)
        unless answer
          warn "Invalid version specification!"
          exit 1
        end
        gem_hadar_version = answer[0]
        filename = "#{name}.gemspec"
        old_data = File.read(filename)
        new_data = old_data.gsub(
          /(add_(?:development_)?dependency\(%q<gem_hadar>, \["~> )([\d.]+)("\])/
        ) { "#$1#{gem_hadar_version}#$3" }
        if old_data == new_data
          warn "#{filename.inspect} already depends on gem_hadar "\
            "#{gem_hadar_version} => Do nothing."
        else
          warn "Upgrading #{filename.inspect} to #{gem_hadar_version}."
          secure_write(filename) do |spec|
            spec.write new_data
          end
        end
      end
    end
  end

  # The gemspec_task method defines a Rake task that generates and writes a
  # gemspec file for the project.
  #
  # This method creates a :gemspec task that depends on the :version task,
  # ensuring the version is set before generating the gemspec. It constructs
  # the filename based on the project name, displays a warning message
  # indicating the file being written, and uses secure_write to create the
  # gemspec file with content generated by the gemspec method.
  def gemspec_task
    desc 'Create a gemspec file'
    task :gemspec => :version do
      filename = "#{name}.gemspec"
      warn "Writing to #{filename.inspect} for #{version}"
      secure_write(filename, gemspec.to_ruby)
    end
  end

  # The package_task method sets up a Rake task for packaging the gem.
  #
  # This method configures a task that creates a package directory, initializes
  # a Gem::PackageTask with the current gem specification, and specifies that
  # tar files should be created. It also includes the files to be packaged by
  # adding gem_files to the package_files attribute of the Gem::PackageTask.
  def package_task
    clean 'pkg'
    Gem::PackageTask.new(gemspec) do |pkg|
      pkg.need_tar      = true
      pkg.package_files += gem_files
    end
  end

  # The install_library_task method executes the installed library task block
  # if it has been defined.
  def install_library_task
    @install_library_block.full?(:call)
  end

  # The test_task method sets up a Rake task for executing the project's test
  # suite.
  #
  # This method configures a Rake task named :test that runs the test suite
  # using Rake::TestTask. It includes the test directory and required paths in
  # the load path, specifies the test files to run, and enables verbose output.
  # The task also conditionally depends on the :compile task if project
  # extensions are present.
  def test_task
    tt =  Rake::TestTask.new(:run_tests) do |t|
      t.libs << test_dir
      t.libs.concat require_paths.to_a
      t.test_files = test_files
      t.verbose    = true
    end
    desc 'Run the tests'
    task :test => [ (:compile if extensions.full?), tt.name ].compact
  end

  # The spec_task method sets up a Rake task for executing RSpec tests.
  #
  # This method configures a :spec task that runs the project's RSpec test
  # suite. It initializes an RSpec::Core::RakeTask with appropriate Ruby
  # options, test pattern, and verbose output. The task also conditionally
  # depends on the :compile task if project extensions are present.
  def spec_task
    defined? ::RSpec::Core::RakeTask or return
    st =  RSpec::Core::RakeTask.new(:run_specs) do |t|
      t.ruby_opts ||= ''
      t.ruby_opts << ' -I' << ([ spec_dir ] + require_paths.to_a).uniq * ':'
      t.pattern = spec_pattern
      t.verbose = true
    end
    task :spec => [ (:compile if extensions.full?), st.name ].compact
  end

  # The rcov_task method sets up a Rake task for executing code coverage tests
  # using RCov.
  #
  # This method configures a :rcov task that runs the project's test suite with
  # RCov to generate code coverage reports. It includes the test directory and
  # required paths in the load path, specifies the test files to run, and
  # enables verbose output. The task also conditionally depends on the :compile
  # task if project extensions are present. If RCov is not available, it
  # displays a warning message suggesting to install RCov.
  def rcov_task
    if defined?(::Rcov)
      rt = ::Rcov::RcovTask.new(:run_rcov) do |t|
        t.libs << test_dir
        t.libs.concat require_paths.to_a
        t.libs.uniq!
        t.test_files = test_files
        t.verbose    = true
        t.rcov_opts  = %W[-x '\\b#{test_dir}\/' -x '\\bgems\/']
      end
      desc 'Run the rcov code coverage tests'
      task :rcov => [ (:compile if extensions.full?), rt.name ].compact
      clobber 'coverage'
    else
      desc 'Run the rcov code coverage tests'
      task :rcov do
        warn "rcov doesn't work for some reason, have you tried 'gem install rcov'?"
      end
    end
  end

  # The version_bump_task method defines Rake tasks for incrementing the gem's
  # version number.
  #
  # This method sets up a hierarchical task structure under the :version
  # namespace:
  #
  # - It creates subtasks in the :version:bump namespace for explicitly bumping
  # major, minor, or build versions.
  # - It also defines a :version:bump task that automatically suggests the
  # appropriate version bump type by analyzing recent changes using AI. The
  # suggestion is based on the git log diff between the previous version and
  # the current HEAD, and it prompts the user for confirmation before applying
  # the bump.
  #
  # The tasks utilize the version_log_diff method to gather change information,
  # the ollama_generate method to get AI-powered suggestions, and the
  # version_bump_to method to perform the actual version update.
  def version_bump_task
    namespace :version do
      namespace :bump do
        desc 'Bump major version'
        task :major do
          version_bump_to(:major)
        end

        desc 'Bump minor version'
        task :minor do
          version_bump_to(:minor)
        end

        desc 'Bump build version'
        task :build do
          version_bump_to(:build)
        end
      end

      desc 'Bump version with AI suggestion'
      task :bump do
        log_diff = version_log_diff(from_version: nil, to_version: 'HEAD')
        system   = xdg_config('version_bump_system_prompt.txt', default_version_bump_system_prompt)
        prompt   = xdg_config('version_bump_prompt.txt', default_version_bump_prompt) % { version:, log_diff: }
        response = ollama_generate(system:, prompt:)
        puts response
        default = nil
        if response =~ /(major|minor|build)\s*$/
          default = $1
        end
        response = ask?(
          'Bump a major, minor, or build version%{default}? ',
          /\A(major|minor|build)\z/,
          default:
        )
        if version_type = response&.[](1)
          version_bump_to(version_type)
        else
          exit 1
        end
      end
    end
  end

  # The version_tag_task method defines a Rake task that creates a Git tag for
  # the current version.
  #
  # This method sets up a :version:tag task under the Rake namespace that
  # creates an annotated Git tag for the project's current version. It checks
  # if a tag with the same name already exists and handles the case where the
  # tag exists but is different from the current commit. If the tag already
  # exists and is different, it prompts the user to confirm whether to
  # overwrite it forcefully.
  def version_tag_task
    namespace :version do
      desc "Tag this commit as version #{version}"
      task :tag do
        force = ENV['FORCE'].to_i == 1
        begin
          sh "git tag -a -m 'Version #{version}' #{'-f' if force} #{version_tag(version)}"
        rescue RuntimeError
          if `git diff v#{version}..HEAD`.empty?
            puts "Version #{version} is already tagged, but it's no different"
          else
            if ask?("Different version tag #{version} already exists. Overwrite with "\
                "force? (yes/NO) ", /\Ayes\z/i)
              force = true
              retry
            else
              exit 1
            end
          end
        end
      end
    end
  end

  # The git_remote method retrieves the primary Git remote name configured for
  # the project.
  #
  # It first checks the GIT_REMOTE environment variable for a custom remote
  # specification. If not set, it defaults to 'origin'. When multiple remotes
  # are specified in the environment variable, only the first one is returned.
  def git_remote
    ENV.fetch('GIT_REMOTE', 'origin').split(/\s+/).first
  end

  # The master_prepare_task method defines a Rake task that sets up a remote
  # Git repository for the project.
  #
  # This method creates a :master:prepare task under the Rake namespace that
  # guides the user through creating a new bare Git repository on a remote
  # server via SSH. It prompts for the remote name, directory path, and SSH
  # account details to configure the repository and establish a connection back
  # to the local project.
  def master_prepare_task
    namespace :master do
      desc "Prepare a remote git repository for this project"
      task :prepare do
        puts "Create a new remote git repository for #{name.inspect}"
        remote_name = ask?('Name (default: origin) ? ', /^.+$/).
          full?(:[], 0) || 'origin'
        dir         = ask?("Directory (default: /git/#{name}.git)? ", /^.+$/).
          full?(:[], 0) || "/git/#{name}.git"
        ssh_account = ask?('SSH account (format: login@host)? ', /^[^@]+@[^@]+/).
          full?(:[], 0) || exit(1)
        sh "ssh #{ssh_account} 'git init --bare #{dir}'"
        sh "git remote add -m master #{remote_name} #{ssh_account}:#{dir}"
      end
    end
  end

  # The version_push_task method defines Rake tasks for pushing version tags to
  # Git remotes.
  #
  # This method sets up a hierarchical task structure under the :version
  # namespace:
  #
  # - It creates subtasks in the :version:push namespace for each configured
  # Git remote, allowing individual pushes to specific remotes.
  # - It also defines a top-level :version:push task that depends on all the
  # individual remote push tasks, enabling a single command to push the version
  # tag to all remotes.
  #
  # The tasks utilize the git_remotes method to determine which remotes are
  # configured and generate appropriate push commands for each one.
  def version_push_task
    namespace :version do
      git_remotes.each do |gr|
        namespace gr.to_sym do
          desc "Push version #{version} to git remote #{gr}"
          task :push do
            sh "git push #{gr} v#{version}"
          end
        end
      end

      desc "Push version #{version} to all git remotes: #{git_remotes * ' '}"
      task :push => git_remotes.map { |gr| :"version:#{gr}:push" }
    end
  end

  # The master_push_task method defines Rake tasks for pushing the master
  # branch to configured Git remotes.
  #
  # This method sets up a hierarchical task structure under the :master namespace:
  #
  # - It creates subtasks in the :master:push namespace for each configured Git
  #   remote, allowing individual pushes to specific remotes.
  # - It also defines a top-level :master:push task that depends on all the individual
  #   remote push tasks, enabling a single command to push the master branch to
  #   all remotes.
  #
  # The tasks utilize the git_remotes method to determine which remotes are
  # configured and generate appropriate push commands for each one.
  def master_push_task
    namespace :master do
      git_remotes.each do |gr|
        namespace gr.to_sym do
          desc "Push master to git remote #{gr}"
          task :push do
            sh "git push #{gr} master"
          end
        end
      end

      desc "Push master #{version} to all git remotes: #{git_remotes * ' '}"
      task :push => git_remotes.map { |gr| :"master:#{gr}:push" }
    end
  end

  # The gem_push_task method defines a Rake task for pushing the generated gem
  # file to RubyGems.
  #
  # This method sets up a :gem:push task under the Rake namespace that handles
  # the process of uploading the gem package to RubyGems. It checks if the
  # project is in developing mode and skips the push operation if so.
  # Otherwise, it verifies the existence of the gem file, prompts the user for
  # confirmation before pushing, and uses the gem push command with an optional
  # API key from the environment. If the gem file does not exist or the user
  # declines to push, appropriate messages are displayed and the task exits
  # accordingly.
  def gem_push_task
    namespace :gem do
      path = "pkg/#{name_version}.gem"
      desc "Push gem file #{File.basename(path)} to rubygems"
      if developing
        task :push => :build do
          puts "Skipping push to rubygems while developing mode is enabled."
        end
      else
        task :push => :build do
          if File.exist?(path)
            if ask?("Do you really want to push #{path.inspect} to rubygems? "\
                "(yes/NO) ", /\Ayes\z/i)
              then
              key = ENV['GEM_HOST_API_KEY'].full? { |k| "--key #{k} " }
              sh "gem push #{key}#{path}"
            else
              exit 1
            end
          else
            warn "Cannot push gem to rubygems: #{path.inspect} doesn't exist."
            exit 1
          end
        end
      end
    end
  end

  # The git_remotes_task method defines a Rake task that displays all Git
  # remote repositories configured for the project.
  #
  # This method sets up a :git_remotes task under the Rake namespace that
  # retrieves and prints the list of Git remotes along with their URLs. It uses
  # the git_remotes method to obtain the remote names and then fetches each
  # remote's URL using the `git remote get-url` command. The output is
  # formatted to show each remote name followed by its corresponding URL on
  # separate lines.
  def git_remotes_task
    task :git_remotes do
      puts git_remotes.map { |r|
        url = `git remote get-url #{r.inspect}`.chomp
        "#{r} #{url}"
      }
    end
  end

  # The create_git_release_body method generates a changelog for a GitHub
  # release by analyzing the git diff between the previous version and the
  # current HEAD.
  #
  # It retrieves the log differences, fetches or uses default system and prompt
  # templates, and utilizes an AI model to produce a formatted changelog entry.
  #
  # @return [ String ] the generated changelog content for the release body
  def create_git_release_body
    log_diff = version_log_diff(to_version: version)
    system   = xdg_config('release_system_prompt.txt', default_git_release_system_prompt)
    prompt   = xdg_config('release_prompt.txt', default_git_release_prompt) % { name:, version:, log_diff: }
    ollama_generate(system:, prompt:)
  end

  # The github_release_task method defines a Rake task that creates a GitHub
  # release for the current version.
  #
  # This method sets up a :github:release task that prompts the user to confirm
  # publishing a release message on GitHub. It retrieves the GitHub API token
  # from the environment, derives the repository owner and name from the git
  # remote URL, generates a changelog using AI, and creates the release via the
  # GitHub API.
  def github_release_task
    namespace :github do
      unless github_api_token = ENV['GITHUB_API_TOKEN'].full?
        warn "GITHUB_API_TOKEN not set. => Skipping github release task."
        task :release
        return
      end
      desc "Create a new GitHub release for the current version with a AI-generated changelog"
      task :release do
        yes = ask?(
          "Do you want to publish a release message on github? (y/n%{default}) ",
          /\Ay/i, default: ENV['GITHUB_RELEASE_ENABLED']
        )
        unless yes
            warn "Skipping publication of a github release message."
            next
        end
        if %r(\A/*(?<owner>[^/]+)/(?<repo>[^/.]+)) =~ github_remote_url&.path
          rc = GitHub::ReleaseCreator.new(owner:, repo:, token: github_api_token)
          tag_name         = version_tag(version)
          target_commitish = `git show -s --format=%H #{tag_name.inspect}^{commit}`.chomp
          body             = edit_temp_file(create_git_release_body)
          if body.present?
            begin
              response = rc.perform(tag_name:, target_commitish:, body:)
              puts "Release created successfully! See #{response.html_url}"
            rescue => e
              warn e.message
            end
          else
            warn "Skipping creation of github release message."
          end
        else
          warn "Could not derive github remote url from git remotes. => Skipping github release task."
        end
      end
    end
  end

  # The push_task_dependencies method manages the list of dependencies for the push task.
  #
  # This method sets up a DSL accessor for the push_task_dependencies attribute,
  # which specifies the sequence of Rake tasks that must be executed when running
  # the push task. These dependencies typically include checks for modified files,
  # building the gem, pushing to remote repositories, and publishing to package
  # managers like RubyGems and GitHub.
  #
  # @return [ Array<Symbol, String> ] an array of task names that are required
  #                                   as dependencies for the push task execution
  dsl_accessor :push_task_dependencies, %i[ modified build master:push version:push gem:push github:release ]

  # The push_task method defines a Rake task that orchestrates the complete
  # process of pushing changes and artifacts to remote repositories and package
  # managers.
  #
  # This method sets up multiple subtasks including preparing the master
  # branch, pushing version tags, pushing to gem repositories, and creating
  # GitHub releases. It also includes a check for uncommitted changes before
  # proceeding with the push operations.
  def push_task
    master_prepare_task
    version_push_task
    master_push_task
    gem_push_task
    git_remotes_task
    github_release_task
    task :modified do
      changed_files = `git status --porcelain`.gsub(/^\s*\S\s+/, '').lines
      unless changed_files.empty?
        warn "There are still modified files:\n#{changed_files * ''}"
        exit 1
      end
    end
    desc "Push all changes for version #{version} into the internets."
    task :push => push_task_dependencies
  end

  # The release_task method defines a Rake task that orchestrates the complete
  # release process for the gem.
  #
  # This method sets up a :release task that depends on the :push task,
  # ensuring all necessary steps for publishing the gem are executed in
  # sequence. It provides a convenient way to perform a full release workflow
  # with a single command.
  def release_task
    desc "Release the new version #{version} for the gem #{name}."
    task :release => :push
  end

  # The compile_task method sets up a Rake task to compile project extensions.
  #
  # This method creates a :compile task that iterates through the configured
  # extensions and compiles them using the system's make command.
  def compile_task
    for file in extensions
      dir = File.dirname(file)
      clean File.join(dir, 'Makefile'), File.join(dir, '*.{bundle,o,so}')
    end
    desc "Compile extensions: #{extensions * ', '}"
    task :compile do
      for file in extensions
        dir, file = File.split(file)
        cd dir do
          ruby file
          sh make
        end
      end
    end
  end

  # The rvm_task method creates a .rvmrc file that configures RVM to use the
  # specified Ruby version and gemset for the project.
  #
  # This task generates a .rvmrc file in the project root directory with commands to:
  # - Use the Ruby version specified by the rvm.use accessor
  # - Create the gemset specified by the rvm.gemset accessor
  # - Switch to using that gemset
  #
  # The generated file is written using the secure_write method to ensure
  # proper file permissions.
  def rvm_task
    desc 'Create .rvmrc file'
    task :rvm do
      secure_write('.rvmrc') do |output|
        output.write <<~EOT
          rvm use #{rvm.use}
          rvm gemset create #{rvm.gemset}
          rvm gemset use #{rvm.gemset}
        EOT
      end
    end
  end

  def yard_doc_task
    YARD::Rake::YardocTask.new(:yard_doc) do |t|
      t.files = files.select { _1 =~ /\.rb\z/ }

      output_dir = yard_dir
      t.options = [ "--output-dir=#{output_dir}" ]

      # Include private & protected methods in documentation
      t.options << '--private' << '--protected'

      # Handle readme if present
      if readme && File.exist?(readme)
        t.options << "--readme=#{readme}"
      end

      # Add additional documentation files
      if doc_files&.any?
        t.files.concat(doc_files.flatten)
      end

      # Add before hook for cleaning
      t.before = proc {
        clean output_dir
        puts "Generating full documentation in #{output_dir}..."
      }
    end
  end

  # The yard_task method sets up and registers Rake tasks for generating and
  # managing YARD documentation.
  #
  # It creates multiple subtasks under the :yard namespace, including tasks for
  # creating private documentation, viewing the generated documentation,
  # cleaning up documentation files, and listing undocumented elements.
  # If YARD is not available, the method returns early without defining any tasks.
  def yard_task
    defined? YARD or return
    yard_doc_task
    desc 'Create yard documentation (including private)'
    task :doc => :yard_doc
    namespace :yard do
      my_yard_dir = Pathname.new(yard_dir)

      task :private => :yard_doc

      task :public => :yard_doc

      desc 'Create yard documentation'
      task :doc => :yard_doc

      desc 'View the yard documentation'
      task :view do
        index_file = my_yard_dir + 'index.html'
        File.exist?(index_file)
        sh "open #{index_file}"
      end

      desc 'Clean the yard documentation'
      task :clean do
        rm_rf my_yard_dir
      end

      desc 'List all undocumented classes/modules/methods'
      task :'list-undoc' do
        sh "yard stats --list-undoc"
      end
    end

    desc 'Create the yard documentation and view it'
    task :yard => %i[ yard:private yard:view ]
  end

  def config_task
    namespace :gem_hadar do
      desc "Display current gem_hadar configuration"
      task :config do
        puts "=== GemHadar Configuration ==="

        # RubyGems
        if ENV['GEM_HOST_API_KEY'].present?
          puts "RubyGems API Key: *** (set)"
        else
          puts "RubyGems API Key: Not set"
        end

        # GitHub
        if ENV['GITHUB_API_TOKEN'].present?
          puts "GitHub API Token: *** (set)"
        else
          puts "GitHub API Token: Not set"
        end

        # Ollama
        puts "Ollama Model: #{ollama_model} (default is #{ollama_model_default})"

        if url = ollama_client&.full?(:base_url)&.to_s
          puts "Ollama Base URL: #{url.inspect}"
        else
          puts "Ollama Base URL: Not set"
        end

        if ENV['OLLAMA_MODEL_OPTIONS']
          puts "Ollama Model Options: #{ENV['OLLAMA_MODEL_OPTIONS']}"
        else
          puts "Ollama Model Options: Not set (using defaults)"
        end

        # XDG config home
        puts "XDG config home: #{xdg_config_home.to_s.inspect}"

        # General
        puts "Gem Name: #{name}"
        puts "Version: #{version}"

        # Build/Development
        puts "MAKE: #{ENV['MAKE'] || 'Not set (will use gmake or make)'}"
        puts "EDITOR: #{ENV['EDITOR'] || 'Not set (will use vi)'}"

        # Git
        puts "Git Remote(s): #{ENV['GIT_REMOTE'] || 'origin'}"

        # Other
        puts "Force Operations: #{ENV['FORCE'] || '0'}"
        puts "Version Override: #{ENV['VERSION'] || 'Not set'}"
        puts "GitHub Release Enabled: #{ENV['GITHUB_RELEASE_ENABLED'] || 'Not set'}"

        puts "\n=== AI Prompt Configuration (Default Values) ==="
        arrow = ?⤵
        puts bold{"version_bump_system_prompt.txt"} + "#{arrow}\n" + italic{default_version_bump_system_prompt}
        puts bold{"version_bump_prompt.txt"} + "#{arrow}\n#{default_version_bump_prompt}"
        puts bold{"release_system_prompt.txt"} + "#{arrow}\n" + italic{default_git_release_system_prompt}
        puts bold{"release_prompt.txt"} + "#{arrow}\n" + italic{default_git_release_prompt}

        puts "=== End Configuration ==="
      end
    end
  end

  # The create_all_tasks method sets up and registers all the Rake tasks for
  # the gem project.
  #
  # @return [GemHadar] the instance of GemHadar
  def create_all_tasks
    default_task
    config_task
    build_task
    rvm_task
    version_task
    version_show_task
    version_diff_task
    gem_hadar_update_task
    gemspec_task
    gems_install_task
    if test_dir
      test_task
      rcov_task
    end
    spec_task
    package_task
    yard_task
    install_library_task
    version_bump_task
    version_tag_task
    push_task
    release_task
    write_ignore_file
    write_gemfile
    if extensions.full?
      compile_task
      task :prepare_install => :compile
    else
      task :prepare_install
    end
    self
  end

  # The edit_temp_file method creates a temporary file with the provided
  # content, opens it in an editor for user modification, and returns the
  # updated content.
  #
  # This method first determines the editor to use from the EDITOR environment
  # variable or defaults to vi. If the editor cannot be found, it issues a
  # warning and returns nil. It then creates a temporary file, writes the
  # initial content to it, and opens the file in the editor. After the user
  # saves and closes the editor, it reads the modified content from the
  # temporary file. The temporary file is automatically cleaned up after use.
  #
  # @param content [ String ] the initial content to write to the temporary
  # file
  #
  # @return [ String, nil ] the content of the temporary file after editing, or
  # nil if the editor could not be found or failed
  def edit_temp_file(content)
    editor = ENV.fetch('EDITOR', `which vi`.chomp)
    unless File.exist?(editor)
      warn "Can't find EDITOR. => Returning."
      return
    end
    temp_file = Tempfile.new(%w[ changelog .md ])
    temp_file.write(content)
    temp_file.close

    unless system("#{editor} #{temp_file.path}")
      warn "#{editor} returned #{$?.exitstatus} => Returning."
      return
    end

    File.read(temp_file.path)
  ensure
    temp_file&.close&.unlink
  end

  # The ollama_model_default method returns the default name of the Ollama AI
  # model to be used for generating responses when no custom model is
  # specified.
  #
  # @return [ String ] the default Ollama AI model name, which is 'llama3.1'
  dsl_accessor :ollama_model_default, 'llama3.1'.freeze

  # The ollama_model method retrieves the name of the Ollama AI model to be
  # used for generating responses.
  #
  # It first checks the OLLAMA_MODEL environment variable for a custom model
  # specification. If the environment variable is not set, it falls back to
  # using the default model name, which is determined by the
  # ollama_model_default dsl method.
  #
  # @return [ String ] the name of the Ollama AI model to be used
  def ollama_model
    ENV.fetch('OLLAMA_MODEL', ollama_model_default)
  end

  # The ollama_client method creates and returns an Ollama::Client instance
  # configured with a base URL derived from environment variables.
  #
  # It first checks for the OLLAMA_URL environment variable to determine the
  # base URL. If that is not set, it falls back to using the OLLAMA_HOST
  # environment variable, defaulting to 'localhost:11434' if that is also not
  # set. The method then constructs the full base URL and initializes an
  # Ollama::Client with appropriate timeouts for read and connect operations.
  #
  # @return [Ollama::Client, nil] An initialized Ollama::Client instance if a valid base URL is present, otherwise nil.
  def ollama_client
    base_url = ENV['OLLAMA_URL']
    if base_url.blank?
      host = ENV.fetch('OLLAMA_HOST', 'localhost:11434')
      base_url = 'http://%s' % host
    end
    base_url.present? or return
    ollama = Ollama::Client.new(base_url:, read_timeout: 600, connect_timeout: 60)
  end

  # Generates a response from an AI model using the Ollama::Client.
  #
  # @param [String] system The system prompt for the AI model.
  # @param [String] prompt The user prompt to generate a response to.
  # @return [String, nil] The generated response or nil if generation fails.
  def ollama_generate(system:, prompt:)
    unless ollama = ollama_client
      warn "Ollama is not configured. => Returning."
      return
    end
    model    = ollama_model
    options  = ENV['OLLAMA_MODEL_OPTIONS'].full? { |o| JSON.parse(o) } || {}
    options |= { "temperature" => 0, "top_p" => 1, "min_p" => 0.1 }
    ollama.generate(model:, system:, prompt:, options:, stream: false, think: false).response
  end

  # Increases the specified part of the version number and writes it back to
  # the VERSION  file.
  #
  # @param [Symbol, String] type The part of the version to bump (:major, :minor, or :build)
  def version_bump_to(type)
    type    = type.to_sym
    version = File.read('VERSION').chomp.version
    version.bump(type)
    secure_write('VERSION') { |v| v.puts version }
    exit 0
  end

  # Determine the start and end versions for diff comparison.
  #
  # If the VERSION env var is set, it will be used as the starting version tag.
  # Otherwise, it defaults to the current commit's version or the latest tag.
  #
  # @return [Array(String, String)] A fixed-size array containing:
  #   - The start version (e.g., '1.2.3') from which changes are compared.
  #   - The end version (e.g., '1.2.4' or 'HEAD') up to which changes are compared.
  def determine_version_range
    version_tags = versions.map { version_tag(_1) } + %w[ HEAD ]
    found_version_tag = version_tags.index(version_tag(version))
    found_version_tag.nil? and fail "cannot find version tag #{version_tag(version)}"
    start_version, end_version = version_tags[found_version_tag, 2]
    return start_version, end_version
  end

  # The write_ignore_file method writes the current ignore_files configuration
  # to a .gitignore file in the project root directory.
  def write_ignore_file
    secure_write('.gitignore') do |output|
      output.puts(ignore.sort)
    end
  end

  # The write_gemfile method creates and writes the default Gemfile content if
  # it doesn't exist. If a custom Gemfile exists, it only displays a warning.
  def write_gemfile
    default_gemfile =<<~EOT
      # vim: set filetype=ruby et sw=2 ts=2:

      source 'https://rubygems.org'

      gemspec
    EOT
    current_gemfile = File.exist?('Gemfile') && File.read('Gemfile')
    case current_gemfile
    when false
      secure_write('Gemfile') do |output|
        output.write default_gemfile
      end
    when default_gemfile
      ;;
    else
      warn "INFO: Current Gemfile differs from default Gemfile."
    end
  end

  # The assert_valid_link method verifies that the provided URL is valid by
  # checking if it returns an HTTP OK status after following redirects, unless
  # project is still `developing`.
  #
  # @param name [String] the name associated with the link being validated
  # @param orig_url [String] the URL to validate
  #
  # @return [String] the original URL if validation succeeds
  #
  # @raise [ArgumentError] if the final response is not an HTTP OK status after
  # following redirects
  def assert_valid_link(name, orig_url)
    developing and return orig_url
    url = orig_url
    begin
      response = Net::HTTP.get_response(URI.parse(url))
      url = response['location']
    end while response.is_a?(Net::HTTPRedirection)
    response.is_a?(Net::HTTPOK) or
      fail "#{orig_url.inspect} for #{name} has to be a valid link"
    orig_url
  end

  # The gemspec method creates and returns a new Gem::Specification object
  # that defines the metadata and dependencies for the gem package.
  #
  # @return [Gem::Specification] a fully configured Gem specification object
  def gemspec
    Gem::Specification.new do |s|
      s.name        = name
      s.version     = ::Gem::Version.new(version)
      s.author      = author
      s.email       = email
      s.homepage    = assert_valid_link(:homepage, homepage)
      s.summary     = summary
      s.description = description

      gem_files.full? { |f| s.files = Array(f) }
      test_files.full? { |t| s.test_files = Array(t) }
      extensions.full? { |e| s.extensions = Array(e) }
      bindir.full? { |b| s.bindir = b }
      executables.full? { |e| s.executables = Array(e) }
      licenses.full? { |l| s.licenses = Array(licenses) }
      post_install_message.full? { |m| s.post_install_message = m }

      required_ruby_version.full? { |v| s.required_ruby_version = v }
      s.add_development_dependency('gem_hadar', "~> #{VERSION[/\A\d+\.\d+/, 0]}")
      for d in @development_dependencies
        s.add_development_dependency(*d)
      end
      for d in @dependencies
        if s.respond_to?(:add_runtime_dependency)
          s.add_runtime_dependency(*d)
        else
          s.add_dependency(*d)
        end
      end

      require_paths.full? { |r| s.require_paths = Array(r) }

      if title
        s.rdoc_options << '--title' << title
      else
        s.rdoc_options << '--title' << "#{name.camelize} - #{summary}"
      end
      if readme
        if File.exist?(readme)
          s.rdoc_options << '--main' << readme
          s.extra_rdoc_files << readme
        else
          warn "Add a #{readme} file to document your gem!"
        end
      end
      doc_files.full? { |df| s.extra_rdoc_files.concat Array(df) }
    end
  end

  # The git_remotes method retrieves the list of remote repositories configured
  # for the current Git project.
  #
  # It first attempts to read the remotes from the ENV['GIT_REMOTE']
  # environment variable, splitting it by whitespace. If this is not available,
  # it falls back to querying the local Git repository using `git remote`.
  #
  # @return [ Array<String> ] an array of remote names
  def git_remotes
    remotes = ENV['GIT_REMOTE'].full?(:split, /\s+/)
    remotes or remotes = `git remote`.lines.map(&:chomp)
    remotes
  end

  # The ask? method prompts the user with a message and reads their input It
  # returns a MatchData object if the input matches the provided pattern.
  #
  # @param prompt [ String ] the message to display to the user
  # @param pattern [ Regexp ] the regular expression to match against the input
  #
  # @return [ MatchData, nil ] the result of the pattern match or nil if no match
  def ask?(prompt, pattern, default: nil)
    if prompt.include?('%{default}')
      if default.present?
        prompt = prompt % { default: ", default is #{default.inspect}" }
      else
        prompt = prompt % { default: '' }
      end
    end
    STDOUT.print prompt
    answer = STDIN.gets.chomp
    default.present? && answer.blank? and answer = default
    if answer =~ pattern
      $~
    end
  end

  # The gem_files method returns an array of files that are included in the gem
  # package.
  #
  # It calculates this by subtracting the files listed in package_ignore_files
  # from the list of all files.
  #
  # @return [ Array<String> ] the list of files to include in the gem package
  def gem_files
    (files.to_a - package_ignore_files.to_a)
  end

  # The versions method retrieves and processes the list of git tags that match
  # semantic versioning patterns.
  #
  # It executes `git tag` to get all available tags, filters them using a
  # regular expression to identify valid version strings, removes any 'v'
  # prefix from each version string, trims whitespace, and sorts the resulting
  # array based on semantic versioning order.
  #
  # @return [ Array<String> ] an array of version strings sorted in ascending
  # order according to semantic versioning rules.
  memoize method:
  def versions
    `git tag`.lines.grep(/^v?\d+\.\d+\.\d+$/).map(&:chomp).map {
      _1.sub(/\Av/, '')
    }.sort_by(&:version)
  end

  # The version_tag method prepends a 'v' prefix to the given version
  # string, unless it's HEAD.
  #
  # @param version [String] the version string to modify
  # @return [String] the modified version string with a 'v' prefix
  def version_tag(version)
    if version != 'HEAD'
      version.dup.prepend ?v
    else
      version.dup
    end
  end

  # The version_untag method removes the 'v' prefix from a version tag string.
  #
  # @param version_tag [ String ] the version tag string that may start with 'v'
  #
  # @return [ String ] the version string with the 'v' prefix removed
  def version_untag(version_tag)
    version.sub(/\Av/, '')
  end

  # The github_remote_url method retrieves and parses the GitHub remote URL
  # from the local Git configuration.
  #
  # It executes `git remote -v` to get all remote configurations, extracts the
  # push URLs, processes them to construct valid URIs, and returns the first
  # URI pointing to GitHub.com.
  #
  # @return [URI, nil] The parsed GitHub remote URI or nil if not found.
  def github_remote_url
    if remotes = `git remote -v`
      remotes_urls = remotes.scan(/^(\S+)\s+(\S+)\s+\(push\)/)
      remotes_uris = remotes_urls.map do |name, url|
        if %r(\A(?<scheme>[^@]+)@(?<hostname>[A-Za-z0-9.]+):(?:\d*)(?<path>.*)) =~ url
          path = ?/ + path unless path.start_with? ?/
          url = 'ssh://%s@%s%s' % [ scheme, hostname, path ] # approximate correct URIs
        end
        URI.parse(url)
      end
      remotes_uris.find { |uri| uri.hostname == 'github.com' }
    end
  end
end

# The GemHadar method serves as the primary entry point for configuring and
# initializing a gem project using the GemHadar framework.
#
# This method creates a new instance of the GemHadar class, passes the provided
# block to configure the gem settings, and then invokes the create_all_tasks
# method to set up all the necessary Rake tasks for the project.
#
# @param block [ Proc ] a configuration block used to define gem properties and settings
#
# @return [ GemHadar ] the configured GemHadar instance after all tasks have been created
def GemHadar(&block)
  GemHadar.new(&block).create_all_tasks
end

# The template method processes an ERB template file and creates a Rake task
# for its compilation.
#
# This method takes a template file path, removes its extension to determine
# the output file name, and sets up a Rake file task that compiles the template
# using the provided block configuration. It ensures the source file has an
# extension and raises an error if not.
#
# @param pathname [ String ] the path to the template file to be processed
#
# @yield [ block ] the configuration block for the template compiler
#
# @return [ Pathname ] the Pathname object representing the destination file path
def template(pathname, &block)
  template_src = Pathname.new(pathname)
  template_dst = template_src.sub_ext('') # ignore ext, we just support erb anyway
  template_src == template_dst and raise ArgumentError,
    "pathname #{pathname.inspect} needs to have a file extension"
  file template_dst.to_s => template_src.to_s do
    GemHadar::TemplateCompiler.new(&block).compile(template_src, template_dst)
  end
  template_dst
end