# frozen_string_literal: true

################################################################################
# For more details on npm version constraints, see:                            #
# https://docs.npmjs.com/misc/semver                                           #
################################################################################

require "dependabot/update_checkers/java_script/npm_and_yarn"
require "dependabot/utils/java_script/version"
require "dependabot/utils/java_script/requirement"

module Dependabot
  module UpdateCheckers
    module JavaScript
      class NpmAndYarn
        class RequirementsUpdater
          VERSION_REGEX = /[0-9]+(?:\.[A-Za-z0-9\-_]+)*/.freeze
          SEPARATOR = /(?<=[a-zA-Z0-9*])[\s|]+(?![\s|-])/.freeze
          ALLOWED_UPDATE_STRATEGIES =
            %i(widen_ranges bump_versions bump_versions_if_necessary).freeze

          def initialize(requirements:, updated_source:, update_strategy:,
                         latest_version:, latest_resolvable_version:)
            @requirements = requirements
            @updated_source = updated_source
            @update_strategy = update_strategy

            check_update_strategy

            if latest_version
              @latest_version = version_class.new(latest_version)
            end

            return unless latest_resolvable_version

            @latest_resolvable_version =
              version_class.new(latest_resolvable_version)
          end

          def updated_requirements
            requirements.map do |req|
              req = req.merge(source: updated_source)
              next req unless latest_resolvable_version
              next initial_req_after_source_change(req) unless req[:requirement]
              next req if req[:requirement].match?(/^([A-Za-uw-z]|v[^\d])/)

              case update_strategy
              when :widen_ranges then widen_requirement(req)
              when :bump_versions then update_version_requirement(req)
              when :bump_versions_if_necessary
                update_version_requirement_if_needed(req)
              else raise "Unexpected update strategy: #{update_strategy}"
              end
            end
          end

          private

          attr_reader :requirements, :updated_source, :update_strategy,
                      :latest_version, :latest_resolvable_version

          def check_update_strategy
            return if ALLOWED_UPDATE_STRATEGIES.include?(update_strategy)

            raise "Unknown update strategy: #{update_strategy}"
          end

          def updating_from_git_to_npm?
            return false unless updated_source.nil?

            original_source = requirements.map { |r| r[:source] }.compact.first
            original_source&.fetch(:type) == "git"
          end

          def initial_req_after_source_change(req)
            return req unless updating_from_git_to_npm?
            return req unless req[:requirement].nil?

            req.merge(requirement: "^#{latest_resolvable_version}")
          end

          def update_version_requirement(req)
            current_requirement = req[:requirement]

            if current_requirement.match?(/(<|-\s)/i)
              ruby_req = ruby_requirements(current_requirement).first
              return req if ruby_req.satisfied_by?(latest_resolvable_version)

              updated_req = update_range_requirement(current_requirement)
              return req.merge(requirement: updated_req)
            end

            reqs = current_requirement.strip.split(SEPARATOR).map(&:strip)
            req.merge(requirement: update_version_string(reqs.first))
          end

          def update_version_requirement_if_needed(req)
            current_requirement = req[:requirement]
            version = latest_resolvable_version
            return req if current_requirement.strip == ""

            ruby_reqs = ruby_requirements(current_requirement)
            return req if ruby_reqs.any? { |r| r.satisfied_by?(version) }

            update_version_requirement(req)
          end

          def widen_requirement(req)
            current_requirement = req[:requirement]
            version = latest_resolvable_version
            return req if current_requirement.strip == ""

            ruby_reqs = ruby_requirements(current_requirement)
            return req if ruby_reqs.any? { |r| r.satisfied_by?(version) }

            reqs = current_requirement.strip.split(SEPARATOR).map(&:strip)

            updated_requirement =
              if reqs.any? { |r| r.match?(/(<|-\s)/i) }
                update_range_requirement(current_requirement)
              elsif current_requirement.strip.split(SEPARATOR).count == 1
                update_version_string(current_requirement)
              else
                current_requirement
              end

            req.merge(requirement: updated_requirement)
          end

          def ruby_requirements(requirement_string)
            Utils::JavaScript::Requirement.
              requirements_array(requirement_string)
          end

          def update_range_requirement(req_string)
            range_requirements =
              req_string.split(SEPARATOR).select { |r| r.match?(/<|(\s+-\s+)/) }

            if range_requirements.count == 1
              range_requirement = range_requirements.first
              versions = range_requirement.scan(VERSION_REGEX)
              upper_bound = versions.map { |v| version_class.new(v) }.max
              new_upper_bound = update_greatest_version(
                upper_bound,
                latest_resolvable_version
              )

              req_string.sub(
                upper_bound.to_s,
                new_upper_bound.to_s
              )
            else
              req_string + " || ^#{latest_resolvable_version}"
            end
          end

          def update_version_string(req_string)
            req_string.
              sub(VERSION_REGEX) do |old_version|
                if old_version.match?(/\d-/) ||
                   latest_resolvable_version.to_s.match?(/\d-/)
                  latest_resolvable_version.to_s
                else
                  old_parts = old_version.split(".")
                  new_parts = latest_resolvable_version.to_s.split(".").
                              first(old_parts.count)
                  new_parts.map.with_index do |part, i|
                    old_parts[i].match?(/^x\b/) ? "x" : part
                  end.join(".")
                end
              end
          end

          def update_greatest_version(old_version, version_to_be_permitted)
            version = version_class.new(old_version)
            version = version.release if version.prerelease?

            index_to_update =
              version.segments.map.with_index { |seg, i| seg.zero? ? 0 : i }.max

            version.segments.map.with_index do |_, index|
              if index < index_to_update
                version_to_be_permitted.segments[index]
              elsif index == index_to_update
                version_to_be_permitted.segments[index] + 1
              else 0
              end
            end.join(".")
          end

          def version_class
            Utils::JavaScript::Version
          end
        end
      end
    end
  end
end