package ls

import (
	"context"
	"slices"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/checker"
	"github.com/microsoft/typescript-go/internal/collections"
	"github.com/microsoft/typescript-go/internal/compiler"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/debug"
)

type ImpExpKind int32

const (
	ImpExpKindUnknown ImpExpKind = iota
	ImpExpKindImport
	ImpExpKindExport
)

type ImportExportSymbol struct {
	kind       ImpExpKind
	symbol     *ast.Symbol
	exportInfo *ExportInfo
}

type ExportKind int

const (
	ExportKindNamed        ExportKind = 0
	ExportKindDefault      ExportKind = 1
	ExportKindExportEquals ExportKind = 2
	ExportKindUMD          ExportKind = 3
	ExportKindModule       ExportKind = 4
)

type ExportInfo struct {
	exportingModuleSymbol *ast.Symbol
	exportKind            ExportKind
}

type LocationAndSymbol struct {
	importLocation *ast.Node
	importSymbol   *ast.Symbol
}

type ImportsResult struct {
	importSearches   []LocationAndSymbol
	singleReferences []*ast.Node
	indirectUsers    []*ast.SourceFile
}

type ImportTracker func(exportSymbol *ast.Symbol, exportInfo *ExportInfo, isForRename bool) *ImportsResult

type ModuleReferenceKind int32

const (
	ModuleReferenceKindImport ModuleReferenceKind = iota
	ModuleReferenceKindReference
	ModuleReferenceKindImplicit
)

// ModuleReference represents a reference to a module, either via import, <reference>, or implicit reference
type ModuleReference struct {
	kind            ModuleReferenceKind
	literal         *ast.Node // for import and implicit kinds (StringLiteralLike)
	referencingFile *ast.SourceFile
	ref             *ast.FileReference // for reference kind
}

// Creates the imports map and returns an ImportTracker that uses it. Call this lazily to avoid calling `getDirectImportsMap` unnecessarily.
func createImportTracker(ctx context.Context, program *compiler.Program, sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[string], checker *checker.Checker) ImportTracker {
	allDirectImports := getDirectImportsMap(ctx, program, sourceFiles, checker)
	return func(exportSymbol *ast.Symbol, exportInfo *ExportInfo, isForRename bool) *ImportsResult {
		directImports, indirectUsers := getImportersForExport(sourceFiles, sourceFilesSet, allDirectImports, exportInfo, checker)
		importSearches, singleReferences := getSearchesFromDirectImports(directImports, exportSymbol, exportInfo.exportKind, checker, isForRename)
		return &ImportsResult{importSearches, singleReferences, indirectUsers}
	}
}

// Returns a map from a module symbol to all import statements that directly reference the module
func getDirectImportsMap(ctx context.Context, program *compiler.Program, sourceFiles []*ast.SourceFile, checker *checker.Checker) map[*ast.Symbol][]*ast.Node {
	result := make(map[*ast.Symbol][]*ast.Node)
	for _, sourceFile := range sourceFiles {
		if ctx.Err() != nil {
			return result
		}
		forEachImport(program, sourceFile, func(importDecl *ast.Node, moduleSpecifier *ast.Node) {
			if moduleSymbol := checker.GetSymbolAtLocation(moduleSpecifier); moduleSymbol != nil {
				result[moduleSymbol] = append(result[moduleSymbol], importDecl)
			}
		})
	}
	return result
}

// Calls `action` for each import, re-export, or require() in a file
func forEachImport(program *compiler.Program, sourceFile *ast.SourceFile, action func(importStatement *ast.Node, imported *ast.Node)) {
	var implicitImports []*ast.LiteralLikeNode
	_, jsxSpecifier := program.GetJSXRuntimeImportSpecifier(sourceFile.Path())
	if jsxSpecifier != nil {
		implicitImports = append(implicitImports, jsxSpecifier)
	}
	importHelpersSpecifier := program.GetImportHelpersImportSpecifier(sourceFile.Path())
	if importHelpersSpecifier != nil {
		implicitImports = append(implicitImports, importHelpersSpecifier)
	}
	if sourceFile.ExternalModuleIndicator != nil || len(sourceFile.Imports())+len(implicitImports) != 0 {
		for _, i := range sourceFile.Imports() {
			action(ast.ImportFromModuleSpecifier(i), i)
		}
		for _, i := range implicitImports {
			action(ast.ImportFromModuleSpecifier(i), i)
		}
	} else {
		forEachPossibleImportOrExportStatement(sourceFile.AsNode(), func(node *ast.Node) bool {
			switch node.Kind {
			case ast.KindExportDeclaration, ast.KindImportDeclaration, ast.KindJSImportDeclaration:
				if specifier := node.ModuleSpecifier(); specifier != nil && ast.IsStringLiteral(specifier) {
					action(node, specifier)
				}
			case ast.KindImportEqualsDeclaration:
				if isExternalModuleImportEquals(node) {
					action(node, node.AsImportEqualsDeclaration().ModuleReference.Expression())
				}
			}
			return false
		})
	}
}

func forEachPossibleImportOrExportStatement(sourceFileLike *ast.Node, action func(statement *ast.Node) bool) bool {
	for _, statement := range getStatementsOfSourceFileLike(sourceFileLike) {
		if action(statement) || isAmbientModuleDeclaration(statement) && forEachPossibleImportOrExportStatement(statement, action) {
			return true
		}
	}
	return false
}

func getSourceFileLikeForImportDeclaration(node *ast.Node) *ast.Node {
	if ast.IsCallExpression(node) || ast.IsJSDocImportTag(node) {
		return ast.GetSourceFileOfNode(node).AsNode()
	}
	parent := node.Parent
	if ast.IsSourceFile(parent) {
		return parent
	}
	debug.Assert(ast.IsModuleBlock(parent) && isAmbientModuleDeclaration(parent.Parent))
	return parent.Parent
}

func isAmbientModuleDeclaration(node *ast.Node) bool {
	return ast.IsModuleDeclaration(node) && ast.IsStringLiteral(node.Name())
}

func getStatementsOfSourceFileLike(node *ast.Node) []*ast.Node {
	if ast.IsSourceFile(node) {
		return node.Statements()
	}
	if body := node.Body(); body != nil {
		return body.Statements()
	}
	return nil
}

func getImportersForExport(
	sourceFiles []*ast.SourceFile,
	sourceFilesSet *collections.Set[string],
	allDirectImports map[*ast.Symbol][]*ast.Node,
	exportInfo *ExportInfo,
	checker *checker.Checker,
) ([]*ast.Node, []*ast.SourceFile) {
	var directImports []*ast.Node
	var indirectUserDeclarations []*ast.Node
	markSeenDirectImport := nodeSeenTracker()
	markSeenIndirectUser := nodeSeenTracker()
	isAvailableThroughGlobal := isSourceFileWithGlobalExports(exportInfo.exportingModuleSymbol.ValueDeclaration)

	getDirectImports := func(moduleSymbol *ast.Symbol) []*ast.Node {
		return allDirectImports[moduleSymbol]
	}

	// Adds a module and all of its transitive dependencies as possible indirect users
	var addIndirectUser func(*ast.Node, bool)
	addIndirectUser = func(sourceFileLike *ast.Node, addTransitiveDependencies bool) {
		// When isAvailableThroughGlobal, getIndirectUsers already returns all source files,
		// so indirectUserDeclarations is never consulted. Nothing to do here.
		if isAvailableThroughGlobal {
			return
		}
		if !markSeenIndirectUser(sourceFileLike) {
			return
		}
		indirectUserDeclarations = append(indirectUserDeclarations, sourceFileLike)
		if !addTransitiveDependencies {
			return
		}
		moduleSymbol := checker.GetMergedSymbol(sourceFileLike.Symbol())
		if moduleSymbol == nil {
			return
		}
		debug.Assert(moduleSymbol.Flags&ast.SymbolFlagsModule != 0)
		for _, directImport := range getDirectImports(moduleSymbol) {
			if !ast.IsImportTypeNode(directImport) {
				addIndirectUser(getSourceFileLikeForImportDeclaration(directImport), true /*addTransitiveDependencies*/)
			}
		}
	}

	isExported := func(node *ast.Node, stopAtAmbientModule bool) bool {
		for node != nil && !(stopAtAmbientModule && isAmbientModuleDeclaration(node)) {
			if ast.HasSyntacticModifier(node, ast.ModifierFlagsExport) {
				return true
			}
			node = node.Parent
		}
		return false
	}

	handleImportCall := func(importCall *ast.Node) {
		top := ast.FindAncestor(importCall, isAmbientModuleDeclaration)
		if top == nil {
			top = ast.GetSourceFileOfNode(importCall).AsNode()
		}
		addIndirectUser(top, isExported(importCall, true /*stopAtAmbientModule*/))
	}

	handleNamespaceImport := func(importDeclaration *ast.Node, name *ast.Node, isReExport bool, alreadyAddedDirect bool) {
		if exportInfo.exportKind == ExportKindExportEquals {
			// This is a direct import, not import-as-namespace.
			if !alreadyAddedDirect {
				directImports = append(directImports, importDeclaration)
			}
		} else if !isAvailableThroughGlobal {
			sourceFileLike := getSourceFileLikeForImportDeclaration(importDeclaration)
			debug.Assert(ast.IsSourceFile(sourceFileLike) || ast.IsModuleDeclaration(sourceFileLike))
			addIndirectUser(sourceFileLike, isReExport || findNamespaceReExports(sourceFileLike, name, checker))
		}
	}

	var handleDirectImports func(*ast.Symbol)
	handleDirectImports = func(exportingModuleSymbol *ast.Symbol) {
		theseDirectImports := getDirectImports(exportingModuleSymbol)
		for _, direct := range theseDirectImports {
			if !markSeenDirectImport(direct) {
				continue
			}
			// !!! cancellation
			switch direct.Kind {
			case ast.KindCallExpression:
				if ast.IsImportCall(direct) {
					handleImportCall(direct)
				} else if !isAvailableThroughGlobal {
					parent := direct.Parent
					if exportInfo.exportKind == ExportKindExportEquals && ast.IsVariableDeclaration(parent) {
						name := parent.Name()
						if ast.IsIdentifier(name) {
							directImports = append(directImports, name)
						}
					}
				}
			case ast.KindIdentifier:
				// Nothing
			case ast.KindImportEqualsDeclaration:
				handleNamespaceImport(direct, direct.Name(), ast.HasSyntacticModifier(direct, ast.ModifierFlagsExport), false /*alreadyAddedDirect*/)
			case ast.KindImportDeclaration, ast.KindJSImportDeclaration, ast.KindJSDocImportTag:
				directImports = append(directImports, direct)
				if importClause := direct.ImportClause(); importClause != nil {
					if namedBindings := importClause.AsImportClause().NamedBindings; namedBindings != nil && ast.IsNamespaceImport(namedBindings) {
						handleNamespaceImport(direct, namedBindings.Name(), false /*isReExport*/, true /*alreadyAddedDirect*/)
						break
					}
				}
				if !isAvailableThroughGlobal && ast.IsDefaultImport(direct) {
					addIndirectUser(getSourceFileLikeForImportDeclaration(direct), false)
					// Add a check for indirect uses to handle synthetic default imports
				}
			case ast.KindExportDeclaration:
				exportClause := direct.AsExportDeclaration().ExportClause
				if exportClause == nil {
					// This is `export * from "foo"`, so imports of this module may import the export too.
					handleDirectImports(getContainingModuleSymbol(direct, checker))
				} else if ast.IsNamespaceExport(exportClause) {
					// `export * as foo from "foo"` add to indirect uses
					addIndirectUser(getSourceFileLikeForImportDeclaration(direct), true /*addTransitiveDependencies*/)
				} else {
					// This is `export { foo } from "foo"` and creates an alias symbol, so recursive search will get handle re-exports.
					directImports = append(directImports, direct)
				}
			case ast.KindImportType:
				// Only check for typeof import('xyz')
				if !isAvailableThroughGlobal && direct.AsImportTypeNode().IsTypeOf && direct.AsImportTypeNode().Qualifier == nil && isExported(direct, false) {
					addIndirectUser(ast.GetSourceFileOfNode(direct).AsNode(), true /*addTransitiveDependencies*/)
				}
				directImports = append(directImports, direct)
			default:
				debug.FailBadSyntaxKind(direct, "Unexpected import kind.")
			}
		}
	}

	getIndirectUsers := func() []*ast.SourceFile {
		if isAvailableThroughGlobal {
			// It has `export as namespace`, so anything could potentially use it.
			return sourceFiles
		}
		// Module augmentations may use this module's exports without importing it.
		for _, decl := range exportInfo.exportingModuleSymbol.Declarations {
			if ast.IsExternalModuleAugmentation(decl) && sourceFilesSet.Has(ast.GetSourceFileOfNode(decl).FileName()) {
				addIndirectUser(decl, false)
			}
		}
		// This may return duplicates (if there are multiple module declarations in a single source file, all importing the same thing as a namespace), but `State.markSearchedSymbol` will handle that.
		return core.Map(indirectUserDeclarations, ast.GetSourceFileOfNode)
	}

	handleDirectImports(exportInfo.exportingModuleSymbol)
	return directImports, getIndirectUsers()
}

func getContainingModuleSymbol(importer *ast.Node, checker *checker.Checker) *ast.Symbol {
	return checker.GetMergedSymbol(getSourceFileLikeForImportDeclaration(importer).Symbol())
}

// Returns 'true' is the namespace 'name' is re-exported from this module, and 'false' if it is only used locally
func findNamespaceReExports(sourceFileLike *ast.Node, name *ast.Node, checker *checker.Checker) bool {
	namespaceImportSymbol := checker.GetSymbolAtLocation(name)
	return forEachPossibleImportOrExportStatement(sourceFileLike, func(statement *ast.Node) bool {
		if !ast.IsExportDeclaration(statement) {
			return false
		}
		exportClause := statement.AsExportDeclaration().ExportClause
		moduleSpecifier := statement.ModuleSpecifier()
		return moduleSpecifier == nil && exportClause != nil && ast.IsNamedExports(exportClause) && core.Some(exportClause.Elements(), func(element *ast.Node) bool {
			return checker.GetExportSpecifierLocalTargetSymbol(element) == namespaceImportSymbol
		})
	})
}

func getSearchesFromDirectImports(
	directImports []*ast.Node,
	exportSymbol *ast.Symbol,
	exportKind ExportKind,
	checker *checker.Checker,
	isForRename bool,
) ([]LocationAndSymbol, []*ast.Node) {
	var importSearches []LocationAndSymbol
	var singleReferences []*ast.Node

	addSearch := func(location *ast.Node, symbol *ast.Symbol) {
		importSearches = append(importSearches, LocationAndSymbol{location, symbol})
	}

	isNameMatch := func(name string) bool {
		// Use name of "default" even in `export =` case because we may have allowSyntheticDefaultImports
		return name == exportSymbol.Name || exportKind != ExportKindNamed && name == ast.InternalSymbolNameDefault
	}

	// `import x = require("./x")` or `import * as x from "./x"`.
	// An `export =` may be imported by this syntax, so it may be a direct import.
	// If it's not a direct import, it will be in `indirectUsers`, so we don't have to do anything here.
	handleNamespaceImportLike := func(importName *ast.Node) {
		// Don't rename an import that already has a different name than the export.
		if exportKind == ExportKindExportEquals && (!isForRename || isNameMatch(importName.Text())) {
			addSearch(importName, checker.GetSymbolAtLocation(importName))
		}
	}

	searchForNamedImport := func(namedBindings *ast.Node) {
		if namedBindings == nil {
			return
		}
		for _, element := range namedBindings.Elements() {
			name := element.Name()
			propertyName := element.PropertyName()
			if !isNameMatch(core.OrElse(propertyName, name).Text()) {
				continue
			}
			if propertyName != nil {
				// This is `import { foo as bar } from "./a"` or `export { foo as bar } from "./a"`. `foo` isn't a local in the file, so just add it as a single reference.
				singleReferences = append(singleReferences, propertyName)
				// If renaming `{ foo as bar }`, don't touch `bar`, just `foo`.
				// But do rename `foo` in ` { default as foo }` if that's the original export name.
				if !isForRename || name.Text() == exportSymbol.Name {
					// Search locally for `bar`.
					addSearch(name, checker.GetSymbolAtLocation(name))
				}
			} else {
				var localSymbol *ast.Symbol
				if ast.IsExportSpecifier(element) && element.PropertyName() != nil {
					localSymbol = checker.GetExportSpecifierLocalTargetSymbol(element)
				} else {
					localSymbol = checker.GetSymbolAtLocation(name)
				}
				addSearch(name, localSymbol)
			}
		}
	}

	handleImport := func(decl *ast.Node) {
		if ast.IsImportEqualsDeclaration(decl) {
			if isExternalModuleImportEquals(decl) {
				handleNamespaceImportLike(decl.Name())
			}
			return
		}
		if ast.IsIdentifier(decl) {
			handleNamespaceImportLike(decl)
			return
		}
		if ast.IsImportTypeNode(decl) {
			if qualifier := decl.AsImportTypeNode().Qualifier; qualifier != nil {
				firstIdentifier := ast.GetFirstIdentifier(qualifier)
				if firstIdentifier.Text() == ast.SymbolName(exportSymbol) {
					singleReferences = append(singleReferences, firstIdentifier)
				}
			} else if exportKind == ExportKindExportEquals {
				singleReferences = append(singleReferences, decl.AsImportTypeNode().Argument.AsLiteralTypeNode().Literal)
			}
			return
		}
		// Ignore if there's a grammar error
		if !ast.IsStringLiteral(decl.ModuleSpecifier()) {
			return
		}
		if ast.IsExportDeclaration(decl) {
			if exportClause := decl.AsExportDeclaration().ExportClause; exportClause != nil && ast.IsNamedExports(exportClause) {
				searchForNamedImport(exportClause)
			}
			return
		}
		if importClause := decl.ImportClause(); importClause != nil {
			if namedBindings := importClause.AsImportClause().NamedBindings; namedBindings != nil {
				switch namedBindings.Kind {
				case ast.KindNamespaceImport:
					handleNamespaceImportLike(namedBindings.Name())
				case ast.KindNamedImports:
					// 'default' might be accessed as a named import `{ default as foo }`.
					if exportKind == ExportKindNamed || exportKind == ExportKindDefault {
						searchForNamedImport(namedBindings)
					}
				}
			}
			// `export =` might be imported by a default import if `--allowSyntheticDefaultImports` is on, so this handles both ExportKind.Default and ExportKind.ExportEquals.
			// If a default import has the same name as the default export, allow to rename it.
			// Given `import f` and `export default function f`, we will rename both, but for `import g` we will rename just that.
			if name := importClause.Name(); name != nil && (exportKind == ExportKindDefault || exportKind == ExportKindExportEquals) && (!isForRename || name.Text() == symbolNameNoDefault(exportSymbol)) {
				defaultImportAlias := checker.GetSymbolAtLocation(name)
				addSearch(name, defaultImportAlias)
			}
		}
	}
	for _, decl := range directImports {
		handleImport(decl)
	}
	return importSearches, singleReferences
}

func getImportOrExportSymbol(node *ast.Node, symbol *ast.Symbol, checker *checker.Checker, comingFromExport bool) *ImportExportSymbol {
	exportInfo := func(symbol *ast.Symbol, kind ExportKind) *ImportExportSymbol {
		if exportInfo := getExportInfo(symbol, kind, checker); exportInfo != nil {
			return &ImportExportSymbol{
				kind:       ImpExpKindExport,
				symbol:     symbol,
				exportInfo: exportInfo,
			}
		}
		return nil
	}

	getExport := func() *ImportExportSymbol {
		getExportAssignmentExport := func(ex *ast.Node) *ImportExportSymbol {
			// Get the symbol for the `export =` node; its parent is the module it's the export of.
			if ex.Symbol().Parent == nil {
				return nil
			}
			exportKind := core.IfElse(ex.AsExportAssignment().IsExportEquals, ExportKindExportEquals, ExportKindDefault)
			return &ImportExportSymbol{
				kind:   ImpExpKindExport,
				symbol: symbol,
				exportInfo: &ExportInfo{
					exportingModuleSymbol: ex.Symbol().Parent,
					exportKind:            exportKind,
				},
			}
		}

		// Not meant for use with export specifiers or export assignment.
		getExportKindForDeclaration := func(node *ast.Node) ExportKind {
			if ast.HasSyntacticModifier(node, ast.ModifierFlagsDefault) {
				return ExportKindDefault
			}
			return ExportKindNamed
		}

		getSpecialPropertyExport := func(node *ast.Node, useLhsSymbol bool) *ImportExportSymbol {
			var kind ExportKind
			switch ast.GetAssignmentDeclarationKind(node) {
			case ast.JSDeclarationKindExportsProperty:
				kind = ExportKindNamed
			case ast.JSDeclarationKindModuleExports:
				kind = ExportKindExportEquals
			default:
				return nil
			}
			sym := symbol
			if useLhsSymbol {
				sym = node.Symbol()
			}
			if sym == nil {
				return nil
			}
			return exportInfo(sym, kind)
		}

		parent := node.Parent
		grandparent := parent.Parent
		if symbol.ExportSymbol != nil {
			if ast.IsPropertyAccessExpression(parent) {
				// When accessing an export of a JS module, there's no alias. The symbol will still be flagged as an export even though we're at the use.
				// So check that we are at the declaration.
				if ast.IsBinaryExpression(grandparent) && slices.Contains(symbol.Declarations, parent) {
					return getSpecialPropertyExport(grandparent, false /*useLhsSymbol*/)
				}
				return nil
			}
			return exportInfo(symbol.ExportSymbol, getExportKindForDeclaration(parent))
		} else {
			exportNode := getExportNode(parent, node)
			switch {
			case exportNode != nil && (ast.HasSyntacticModifier(exportNode, ast.ModifierFlagsExport) || ast.IsImplicitlyExportedJSDocDeclaration(exportNode)):
				if ast.IsImportEqualsDeclaration(exportNode) && exportNode.AsImportEqualsDeclaration().ModuleReference == node {
					// We're at `Y` in `export import X = Y`. This is not the exported symbol, the left-hand-side is. So treat this as an import statement.
					if comingFromExport {
						return nil
					}
					lhsSymbol := checker.GetSymbolAtLocation(exportNode.Name())
					return &ImportExportSymbol{
						kind:   ImpExpKindImport,
						symbol: lhsSymbol,
					}
				}
				return exportInfo(symbol, getExportKindForDeclaration(exportNode))
			case ast.IsNamespaceExport(parent):
				return exportInfo(symbol, ExportKindNamed)
			case ast.IsExportAssignment(parent):
				return getExportAssignmentExport(parent)
			case ast.IsExportAssignment(grandparent):
				return getExportAssignmentExport(grandparent)
			case ast.IsBinaryExpression(parent):
				return getSpecialPropertyExport(parent, true /*useLhsSymbol*/)
			case ast.IsBinaryExpression(grandparent):
				return getSpecialPropertyExport(grandparent, true /*useLhsSymbol*/)
			case ast.IsJSDocTypedefTag(parent) || ast.IsJSDocCallbackTag(parent):
				return exportInfo(symbol, ExportKindNamed)
			}
		}
		return nil
	}

	getImport := func() *ImportExportSymbol {
		if !isNodeImport(node) {
			return nil
		}
		// JS destructuring from `require(...)` is import-like for references, but the binding element
		// itself is still a local variable symbol rather than an alias.
		var importedSymbol *ast.Symbol
		if symbol.Flags&ast.SymbolFlagsAlias != 0 {
			importedSymbol = checker.GetImmediateAliasedSymbol(symbol)
		} else {
			importedSymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, checker)
		}
		if importedSymbol == nil {
			return nil
		}
		// Search on the local symbol in the exporting module, not the exported symbol.
		importedSymbol = skipExportSpecifierSymbol(importedSymbol, checker)
		if importedSymbol == nil {
			return nil
		}
		// Similarly, skip past the symbol for 'export ='
		if importedSymbol.Name == "export=" {
			importedSymbol = getExportEqualsLocalSymbol(importedSymbol, checker)
			if importedSymbol == nil {
				return nil
			}
		}
		// If the import has a different name than the export, do not continue searching.
		// If `importedName` is undefined, do continue searching as the export is anonymous.
		// (All imports returned from this function will be ignored anyway if we are in rename and this is a not a named export.)
		importedName := symbolNameNoDefault(importedSymbol)
		if importedName == "" || importedName == ast.InternalSymbolNameDefault || importedName == symbol.Name {
			return &ImportExportSymbol{
				kind:   ImpExpKindImport,
				symbol: importedSymbol,
			}
		}
		return nil
	}

	result := getExport()
	if result == nil && !comingFromExport {
		result = getImport()
	}
	return result
}

func getExportInfo(exportSymbol *ast.Symbol, exportKind ExportKind, c *checker.Checker) *ExportInfo {
	// Parent can be nil if an `export` is not at the top-level (which is a compile error).
	if exportSymbol.Parent != nil {
		exportingModuleSymbol := c.GetMergedSymbol(exportSymbol.Parent)
		// `export` may appear in a namespace. In that case, just rely on global search.
		if checker.IsExternalModuleSymbol(exportingModuleSymbol) {
			return &ExportInfo{
				exportingModuleSymbol: exportingModuleSymbol,
				exportKind:            exportKind,
			}
		}
	}
	return nil
}

// If a reference is a class expression, the exported node would be its parent.
// If a reference is a variable declaration, the exported node would be the variable statement.
func getExportNode(parent *ast.Node, node *ast.Node) *ast.Node {
	var declaration *ast.Node
	switch {
	case ast.IsVariableDeclaration(parent):
		declaration = parent
	case ast.IsBindingElement(parent):
		declaration = ast.WalkUpBindingElementsAndPatterns(parent)
	}
	if declaration != nil {
		if parent.Name() == node && !ast.IsCatchClause(declaration.Parent) && ast.IsVariableStatement(declaration.Parent.Parent) {
			return declaration.Parent.Parent
		}
		return nil
	}
	return parent
}

func isNodeImport(node *ast.Node) bool {
	parent := node.Parent
	switch parent.Kind {
	case ast.KindImportEqualsDeclaration:
		return parent.Name() == node && isExternalModuleImportEquals(parent)
	case ast.KindImportSpecifier:
		// For a rename import `{ foo as bar }`, don't search for the imported symbol. Just find local uses of `bar`.
		return parent.PropertyName() == nil
	case ast.KindImportClause, ast.KindNamespaceImport:
		debug.Assert(parent.Name() == node)
		return true
	case ast.KindBindingElement:
		return ast.IsInJSFile(node) && ast.IsVariableDeclarationInitializedToBareOrAccessedRequire(parent.Parent.Parent)
	}
	return false
}

func isExternalModuleImportEquals(node *ast.Node) bool {
	moduleReference := node.AsImportEqualsDeclaration().ModuleReference
	return ast.IsExternalModuleReference(moduleReference) && moduleReference.Expression().Kind == ast.KindStringLiteral
}

// If at an export specifier, go to the symbol it refers to. */
func skipExportSpecifierSymbol(symbol *ast.Symbol, checker *checker.Checker) *ast.Symbol {
	// For `export { foo } from './bar", there's nothing to skip, because it does not create a new alias. But `export { foo } does.
	for _, declaration := range symbol.Declarations {
		switch {
		case ast.IsExportSpecifier(declaration) && declaration.PropertyName() == nil && declaration.Parent.Parent.ModuleSpecifier() == nil:
			return core.OrElse(checker.GetExportSpecifierLocalTargetSymbol(declaration), symbol)
		case ast.IsPropertyAccessExpression(declaration) && ast.IsModuleExportsAccessExpression(declaration.Expression()) && !ast.IsPrivateIdentifier(declaration.Name()):
			// Export of form 'module.exports.propName = expr';
			return checker.GetSymbolAtLocation(declaration)
		case ast.IsShorthandPropertyAssignment(declaration) && ast.IsBinaryExpression(declaration.Parent.Parent) && ast.GetAssignmentDeclarationKind(declaration.Parent.Parent) == ast.JSDeclarationKindModuleExports:
			return checker.GetExportSpecifierLocalTargetSymbol(declaration.Name())
		}
	}
	return symbol
}

func getExportEqualsLocalSymbol(importedSymbol *ast.Symbol, checker *checker.Checker) *ast.Symbol {
	if importedSymbol.Flags&ast.SymbolFlagsAlias != 0 {
		return checker.GetImmediateAliasedSymbol(importedSymbol)
	}
	decl := importedSymbol.ValueDeclaration
	debug.Assert(decl != nil)
	switch {
	case ast.IsExportAssignment(decl):
		return decl.Expression().Symbol()
	case ast.IsBinaryExpression(decl):
		return decl.AsBinaryExpression().Right.Symbol()
	case ast.IsSourceFile(decl):
		return decl.Symbol()
	}
	return nil
}

func symbolNameNoDefault(symbol *ast.Symbol) string {
	if symbol.Name != ast.InternalSymbolNameDefault {
		return symbol.Name
	}
	for _, decl := range symbol.Declarations {
		name := ast.GetNameOfDeclaration(decl)
		if name != nil && ast.IsIdentifier(name) {
			return name.Text()
		}
	}
	return ""
}

// findModuleReferences finds all references to a module symbol across the given source files.
// This includes import statements, <reference> directives, and implicit references (e.g., JSX runtime imports).
func findModuleReferences(program *compiler.Program, sourceFiles []*ast.SourceFile, searchModuleSymbol *ast.Symbol, checker *checker.Checker) []ModuleReference {
	refs := []ModuleReference{}

	for _, referencingFile := range sourceFiles {
		searchSourceFile := searchModuleSymbol.ValueDeclaration
		if searchSourceFile != nil && searchSourceFile.Kind == ast.KindSourceFile {
			// Check <reference path> directives
			for _, ref := range referencingFile.ReferencedFiles {
				if program.GetSourceFileFromReference(referencingFile, ref) == searchSourceFile.AsSourceFile() {
					refs = append(refs, ModuleReference{
						kind:            ModuleReferenceKindReference,
						referencingFile: referencingFile,
						ref:             ref,
					})
				}
			}

			// Check <reference types> directives
			for _, ref := range referencingFile.TypeReferenceDirectives {
				referenced := program.GetResolvedTypeReferenceDirectiveFromTypeReferenceDirective(ref, referencingFile)
				if referenced != nil && referenced.ResolvedFileName == searchSourceFile.AsSourceFile().FileName() {
					refs = append(refs, ModuleReference{
						kind:            ModuleReferenceKindReference,
						referencingFile: referencingFile,
						ref:             ref,
					})
				}
			}
		}

		// Check all imports (including require() calls)
		forEachImport(program, referencingFile, func(importDecl *ast.Node, moduleSpecifier *ast.Node) {
			moduleSymbol := checker.GetSymbolAtLocation(moduleSpecifier)
			if moduleSymbol == searchModuleSymbol {
				if ast.NodeIsSynthesized(importDecl) {
					refs = append(refs, ModuleReference{
						kind:            ModuleReferenceKindImplicit,
						literal:         moduleSpecifier,
						referencingFile: referencingFile,
					})
				} else {
					refs = append(refs, ModuleReference{
						kind:    ModuleReferenceKindImport,
						literal: moduleSpecifier,
					})
				}
			}
		})
	}

	return refs
}
