# typed: strict
# frozen_string_literal: true

module Packwerk
  # Extracts a possible constant reference from a given AST node.
  class ReferenceExtractor
    extend T::Sig

    class << self
      extend T::Sig

      sig do
        params(
          unresolved_references: T::Array[UnresolvedReference],
          context_provider: ConstantDiscovery
        ).returns(T::Array[Reference])
      end
      def get_fully_qualified_references_from(unresolved_references, context_provider)
        fully_qualified_references = T.let([], T::Array[Reference])

        unresolved_references.each do |unresolved_references_or_offense|
          unresolved_reference = unresolved_references_or_offense

          constant =
            context_provider.context_for(
              unresolved_reference.constant_name,
              current_namespace_path: unresolved_reference.namespace_path
            )

          next if constant.nil?

          package_for_constant = constant.package

          next if package_for_constant.nil?

          source_package = context_provider.package_from_path(unresolved_reference.relative_path)

          next if source_package == package_for_constant

          fully_qualified_references << Reference.new(
            package: source_package,
            relative_path: unresolved_reference.relative_path,
            constant: constant,
            source_location: unresolved_reference.source_location,
          )
        end

        fully_qualified_references
      end
    end

    sig do
      params(
        constant_name_inspectors: T::Array[ConstantNameInspector],
        root_node: AST::Node,
        root_path: String,
      ).void
    end
    def initialize(
      constant_name_inspectors:,
      root_node:,
      root_path:
    )
      @constant_name_inspectors = constant_name_inspectors
      @root_path = root_path
      @local_constant_definitions = T.let(
        ParsedConstantDefinitions.new(root_node: root_node),
        ParsedConstantDefinitions,
      )
    end

    sig do
      params(
        node: Parser::AST::Node,
        ancestors: T::Array[Parser::AST::Node],
        relative_file: String
      ).returns(T.nilable(UnresolvedReference))
    end
    def reference_from_node(node, ancestors:, relative_file:)
      constant_name = T.let(nil, T.nilable(String))

      @constant_name_inspectors.each do |inspector|
        constant_name = inspect_node(
          inspector,
          node: node,
          ancestors: ancestors,
          relative_file: relative_file
        )

        break if constant_name
      end

      if constant_name
        reference_from_constant(
          constant_name,
          node: node,
          ancestors: ancestors,
          relative_file: relative_file
        )
      end
    end

    private

    sig do
      params(
        inspector: ConstantNameInspector,
        node: Parser::AST::Node,
        ancestors: T::Array[Parser::AST::Node],
        relative_file: String
      ).returns(T.nilable(String))
    end
    def inspect_node(inspector, node:, ancestors:, relative_file:)
      inspector.constant_name_from_node(node, ancestors: ancestors, relative_file: relative_file)
    rescue ArgumentError => error
      if error.message == "unknown keyword: :relative_file"
        T.unsafe(inspector).constant_name_from_node(node, ancestors: ancestors).tap do
          warn(<<~MSG.squish)
            #{T.cast(inspector, Object).class}#reference_from_node without a relative_file: keyword
            argument is deprecated and will be required in Packwerk 3.1.1.
          MSG
        end
      else
        raise
      end
    end

    sig do
      params(
        constant_name: String,
        node: Parser::AST::Node,
        ancestors: T::Array[Parser::AST::Node],
        relative_file: String
      ).returns(T.nilable(UnresolvedReference))
    end
    def reference_from_constant(constant_name, node:, ancestors:, relative_file:)
      namespace_path = NodeHelpers.enclosing_namespace_path(node, ancestors: ancestors)

      return if local_reference?(constant_name, NodeHelpers.name_location(node), namespace_path)

      location = NodeHelpers.location(node)

      UnresolvedReference.new(
        constant_name: constant_name,
        namespace_path: namespace_path,
        relative_path: relative_file,
        source_location: location
      )
    end

    sig do
      params(
        constant_name: String,
        name_location: T.nilable(Node::Location),
        namespace_path: T::Array[String],
      ).returns(T::Boolean)
    end
    def local_reference?(constant_name, name_location, namespace_path)
      @local_constant_definitions.local_reference?(
        constant_name,
        location: name_location,
        namespace_path: namespace_path
      )
    end
  end

  private_constant :ReferenceExtractor
end