# frozen_string_literal: true

require 'semantic_puppet'
require 'set'

require_relative '../../../bolt/error'

# This class represents a Forge module specification.
#
module Bolt
  class ModuleInstaller
    class Specs
      class ForgeSpec
        NAME_REGEX    = %r{\A[a-zA-Z0-9]+[-/](?<name>[a-z][a-z0-9_]*)\z}.freeze
        REQUIRED_KEYS = Set.new(%w[name]).freeze
        KNOWN_KEYS    = Set.new(%w[name resolve version_requirement]).freeze

        attr_reader :full_name, :name, :resolve, :semantic_version, :type, :version_requirement

        def initialize(init_hash)
          @resolve                                = init_hash.key?('resolve') ? init_hash['resolve'] : true
          @full_name, @name                       = parse_name(init_hash['name'])
          @version_requirement, @semantic_version = parse_version_requirement(init_hash['version_requirement'])
          @type                                   = :forge

          unless @resolve == true || @resolve == false
            raise Bolt::ValidationError,
                  "Option 'resolve' for module spec #{@full_name} must be a Boolean"
          end
        end

        def self.implements?(hash)
          KNOWN_KEYS.superset?(hash.keys.to_set) && REQUIRED_KEYS.subset?(hash.keys.to_set)
        end

        # Formats the full name and extracts the module name.
        #
        private def parse_name(name)
          unless (match = name.match(NAME_REGEX))
            raise Bolt::ValidationError,
                  "Invalid name for Forge module specification: #{name}. Name must match "\
                  "'owner/name'. Owner segment can only include letters or digits. Name "\
                  "segment must start with a lowercase letter and can only include lowercase "\
                  "letters, digits, and underscores."
          end

          [name.tr('-', '/'), match[:name]]
        end

        # Parses the version into a Semantic Puppet version range.
        #
        private def parse_version_requirement(version_requirement)
          [version_requirement, SemanticPuppet::VersionRange.parse(version_requirement || '>= 0')]
        rescue StandardError
          raise Bolt::ValidationError,
                "Invalid version requirement for Forge module specification #{@full_name}: "\
                "#{version_requirement.inspect}"
        end

        # Returns true if the specification is satisfied by the module.
        #
        def satisfied_by?(mod)
          @type == mod.type &&
            @full_name.downcase == mod.full_name.downcase &&
            !mod.version.nil? &&
            @semantic_version.cover?(mod.version)
        end

        # Returns a hash matching the module spec in bolt-project.yaml
        #
        def to_hash
          {
            'name'                => @full_name,
            'version_requirement' => @version_requirement
          }.compact
        end

        # Creates a PuppetfileResolver::Puppetfile::ForgeModule object, which is
        # used to generate a graph of resolved modules.
        #
        def to_resolver_module
          require 'puppetfile-resolver'

          PuppetfileResolver::Puppetfile::ForgeModule.new(@full_name).tap do |mod|
            mod.version = @version_requirement
          end
        end
      end
    end
  end
end