require 'pathname'

# Configuration related logic.
#
# The most important thing is the expected configuration format, documented in `Kit::Doc::Services::Config.create_config`.
#
module Kit::Doc::Services::Config

  class << self

    # This Service currently acts as a singleton to reference the current config for Yard rake tasks.
    #
    # This is obviously not ideal / thread safe, but it is only used for tooling, so it does not matter.
    attr_accessor :config

  end

  # A regex that match semantic versioning (semver) versions.
  #
  # Note that this is a more permissive version to match semver inside strings and allow for leading `v`.
  #
  # ### References:
  # - https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
  #
  SEMVER_REGEX = %r{(v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)}

  # Configuration normalization for `Kit::Doc`.
  #
  # `opts` is a hash that can contain the following keys:
  #
  #   * `:project_path` - Absolute path of the project home directory.
  #
  #   * `:git_project_path` - Absolute path of the project git home directory.
  #     It will sometimes be different from `:project_path` in a multi-project setup.
  #     This is necessary when using `all_versions` tasks, that will checkout different versions of the project.
  #
  #   * `:project` - Project name.
  #     Autofilled from gemspec if `:gemspec_name` is set.
  #
  #   * `:current_version` - Semver name of the current version. Example: "0.1.0".
  #     Defaults to `dev` (this is the most common usage: previewing new documentation).
  #
  #   * `:source_ref` - The branch/commit/tag used for source link inference.
  #     Autogenerated if nil. If `:git_project_path` is set, the current branch be used if found, last commit otherwise.
  #     Defaults to `main` otherwise.
  #
  #   * `:current_version_display` - Version that will be displayed.
  #     Autogenerated if nil: add `v` in front of `:version` values that start with an integer.
  #     Example: `version: '0.1.0'` would lead to `version_display: 'v0.1.0'`.
  #
  #   * `:all_versions` - List of version & source_ref to be used for `all_versions` tasks and javascript `versionNodes`.
  #     Example: `[{ version: 'edge', source_ref: 'main' }, { version: 'v0.0.1', source_ref: 'v0.0.1' }]`
  #     If a string is given, assume it is the path to a `VERSIONS` file that will be loaded.
  #
  #   * `:output_dir_all_versions` - Container directory for various versioned generated docs.
  #     Defaults to `"#{ project_path }/docs/dist"`.
  #     If a relative path is provided it will be auto-expanded to an absolute path.
  #
  #   * `:output_dir_current_version` - Output directory for the generated docs.
  #     If a relative path is provided it will be auto-expanded to an absolute path.
  #     Auto-generated from `:output_dir_all_versions` and `:current_version` if nil.
  #
  #   * `:documentation_url` - The address of the generated documentation.
  #     Autofilled from gemspec if `:gemspec_name` is set.
  #
  #   * `:source_url` - The address of code source.
  #     Autofilled from gemspec if `:gemspec_name` is set.
  #
  #   * `:url_mode` - Defaults to `:local`.
  #     If `:local`, `:documentation_url` is set to `file://` URI scheme. Useful for browsing `dev` documentation.
  #     (TODO: apply to `source_url` also when we find a good way to preview code locally. `view-source` URI scheme does not have syntax highligthing or line number anchors.)
  #
  #   * `:files_modules` - List of files to generate documentation from.
  #     See `Kit::Doc::Services::Tasks::Helpers.resolve_files` for a helper to express that list in a terser way.
  #
  #   * `:groups_for_modules` - See the "Groups" section
  #
  #   * `:files_extras` - List of Markdown files to add to the documentation.
  #     See `Kit::Doc::Services::Tasks::Helpers.resolve_files` for a helper to express that list in a terser way.
  #
  #   * `:groups_for_extras` - See the "Groups" section
  #
  #   * `:extras_section_name` - String that defines the section title of the additional Markdown pages.
  #     Defaults to "Guides".
  #
  #   * `:logo` - Path to the image logo of the project.
  #
  #   * `:authors` - List of authors for the generated docs.
  #     Autofilled from gemspec if `:gemspec_name` is set.
  #
  #   * `:markdown_variables` - Hash of values available in Markdown pre-processing. See `Kit::Doc::Services::MarkdownPreprocessor`.
  #
  #   * `:assets` - List of paths to files or directories that will be copied as is to the "assets" directory in the output path.
  #
  #   * `:main_redirect_url` - Path to the default page for the documentation.
  #     The top level `index.html` for the `:current_version` documentation will redirect there.
  #     Defaults to "api_reference.html".
  #
  # ## Groups
  #
  # The output sidebar content can be organized in groups. This is done via the `:groups_for_extras`
  # and `:groups_for_modules`. For example, imagine you are storing extra guides in
  # your documentation which are organized per directory. In the extras section you have:
  #
  # ```ruby
  # extras: [
  #   "guides/introduction/foo.md",
  #   "guides/introduction/bar.md",
  #   # ...
  #   "guides/advanced/baz.md",
  #   "guides/advanced/bat.md",
  # ],
  # ```
  #
  # You can have those grouped as follows:
  #
  # ```ruby
  # groups_for_extras: {
  #   'Introduction' => [%r{guides/introduction/.?}],
  #   'Advanced'     => [%r{guides/advanced/.?}],
  # },
  # ```
  #
  # Similar can be done for modules:
  #
  # ```ruby
  # groups_for_modules: {
  #   # The "empty" group is where modules not included in another group go.
  #   ''          => [
  #     {
  #       # Here we define the ones we want to hide from that category.
  #       inclusion: %r{^(Kit|Kit::Doc|Kit::Doc::Services)$},
  #       display:   false,
  #     },
  #   ],
  #   'Services'  => [
  #     {
  #       # Match on the fully qualified name.
  #       inclusion:     %r{^Kit::Doc::Services::.*},
  #       # A callable that will receive and modify the display name of the module.
  #       display_title: ->(name) { name.split('::')[-1] },
  #       # A callable that will the name of the module and return a list of css classes to be applied.
  #       # This can for instance be used to add padding.
  #       css_classes:   ->(name) do
  #         size = name.gsub('hide', 'Kit::Doc::Services::').scan('::').size
  #         ["#{ (size > 0) ? "sidebar-pl-#{ size }" : '' }"]
  #       end,
  #     },
  #   ],
  # ```
  #
  def self.create_config(**opts)
    defaults = {
      # project_path:            nil,
      # git_project_path:        nil,
      # project:                 nil,

      current_version:         ENV['KIT_DOC_CURRENT_VERSION'].presence || 'dev',
      source_ref:              ENV['KIT_DOC_SOURCE_REF'].presence,
      # current_version_display: nil,
      all_versions:            [],

      output_dir_all_versions: ENV['KIT_DOC_OUTPUT_DIR_ALL_VERSIONS'].presence || 'docs/dist',
      # output_dir_current_version: nil

      # documentation_url:       nil,
      # source_url:              nil,
      url_mode:                :local,

      files_modules:           [],
      groups_for_modules:      {},
      files_extras:            [],
      groups_for_extras:       {},

      extras_section_name:     'Guides',
      logo:                    nil,
      authors:                 [],
      markdown_variables:      {},

      assets:                  [],
      main_redirect_url:       'api_reference.html',
    }

    config = opts.dup
    defaults.each { |k, v| config[k] = v if !config[k] }
    config[:url_mode] = config[:url_mode].to_sym

    if !config[:source_ref]
      config[:source_ref] = Kit::Doc::Services::Config.get_git_source_ref(git_project_path: config[:git_project_path])[1][:source_ref]
    end

    if config[:all_versions].is_a?(String)
      config[:all_versions] = Kit::Doc::Services::Config.load_versions_file(path: config[:all_versions])[1][:versions]
    end

    if config[:files_modules].is_a?(Hash)
      config[:files_modules] = Kit::Doc::Services::Tasks::Helpers.resolve_files(hash: config[:files_modules])
    end

    if config[:files_extras].is_a?(Hash)
      config[:files_extras] = Kit::Doc::Services::Tasks::Helpers.resolve_files(hash: config[:files_extras])
    end

    if config[:gemspec_name]
      gemspec_data       = load_gemspec_data(gemspec_name: config[:gemspec_name])[1][:gemspec_data]
      gemspec_attributes = {
        project: gemspec_data.name,
        authors: [gemspec_data.author],
      }

      gemspec_attributes.merge!({
        version:    gemspec_data.version,
        source_url: gemspec_data.metadata['source_code_uri'],
      })

      if config[:url_mode] != :local
        # NOTE: when we have a local source viewer or the view-source uri scheme improve, add `source_url` here too.
        gemspec_attributes.merge!({
          documentation_url: ->(version:) { gemspec_data.metadata['documentation_uri'].gsub(SEMVER_REGEX, version) },
        })
      end

      gemspec_attributes.each { |k, v| config[k] = v if !config[k] }
    end

    if !Pathname.new(config[:output_dir_all_versions]).absolute?
      config[:output_dir_all_versions] = "#{ config[:project_path] }/#{ config[:output_dir_all_versions] }"
    end

    if !config[:output_dir_current_version]
      config[:output_dir_current_version] = "#{ config[:output_dir_all_versions] }/#{ config[:current_version] }"
    end

    if !Pathname.new(config[:output_dir_current_version]).absolute?
      config[:output_dir_current_version] = "#{ config[:project_path] }/#{ config[:output_dir_current_version] }"
    end

    if config[:documentation_url].is_a?(String)
      documentation_url_saved_value = config[:documentation_url]
      config[:documentation_url] = ->(version:) { documentation_url_saved_value }
    elsif config[:url_mode] == :local
      config[:documentation_url] = ->(version:) { "file://#{ config[:output_dir_all_versions] }/#{ version }/index.html" }
    elsif !config[:documentation_url].respond_to?(:call)
      config[:documentation_url] = ->(version:) { '' }
    end

    if !config[:source_url].respond_to?(:call)
      source_url_saved_value = config[:source_url]
      config[:source_url]    = ->(path:, line:) do
        Kit::Doc::Services::Config.generate_source_url(
          source_url: source_url_saved_value,
          source_ref: config[:source_ref],
          path:       path,
          line:       line,
        )
      end
    end

    config[:current_version_display] = "#{ (config[:current_version] =~ %r{^[0-9]}) ? 'v' : '' }#{ config[:current_version] }"

    markdown_variables = {
      CURRENT_VERSION: config[:current_version],
    }
    markdown_variables.each { |k, v| config[:markdown_variables][k] = v if !config[:markdown_variables][k] }

    config
  end

  # Generate a versioned source link for a given `source_ref` & `path` & `line` number.
  def self.generate_source_url(source_url:, source_ref:, path:, line:)
    return '' if !source_url

    # TODO: add support for other platform if requested.
    separators = {
      'github.com/' => ->(separator) do
        url_split     = source_url.chomp('/').split(separator)
        path_split    = url_split[1].split('/')

        path_split[2] = 'blob'
        path_split[3] = source_ref

        path_and_line = ''
        path_and_line << "#{ path }"   if path
        path_and_line << "#L#{ line }" if line
        path_split << path_and_line

        url_split[1] = path_split.join('/')

        url_split.join(separator)
      end,
    }

    separators.each do |separator, fn|
      if source_url.include?(separator)
        return fn.call(separator)
      end
    end
  end

  # Load a gemspec file data.
  def self.load_gemspec_data(gemspec_name:)
    gemspec_data = Gem::Specification.find_by_name(gemspec_name)

    [:ok, gemspec_data: gemspec_data]
  end

  # Parse a `VERSIONS` file.
  #
  # It contains all versions we want to generate documentation for.
  #
  # ### Expected format
  # On each line:
  # ```
  # version[:source_ref]`
  # ```
  #
  #   * `version` - The name of the version.
  #
  #   * `source_ref` - The branch/commit/tag used for source link inference.
  #     Defaults to `version` if not specified.
  #
  # ### Example
  # ```text
  # edge:main
  # v0.0.1
  # ```
  #
  def self.load_versions_file(path:)
    content  = File.readlines(path)
    versions = content.filter_map do |line|
      line = line.presence
      next if !line

      line = line.strip.split(':')

      { version: line[0], source_ref: line[1] || line[0] }
    end

    [:ok, versions: versions]
  end

  # Attempt to guess the most accurate git `source_ref`.
  #
  # If `git_project_path` is set, it will attempt to find the current branch first, then the last commit.
  # Otherwise, defaults to `main`.
  def self.get_git_source_ref(git_project_path:)
    source_ref = nil

    if git_project_path
      begin
        git_data   = Git.open(git_project_path)
        source_ref = git_data.current_branch

        if source_ref.include?('detached')
          source_ref = git_data.log.first.objectish
        end
      rescue ArgumentError
        # TODO: should this be reported in config ?
      end
    end

    source_ref ||= 'main'

    [:ok, source_ref: source_ref]
  end

end
