package api

import (
	"context"
	"encoding/base64"
	"fmt"
	"slices"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"

	"github.com/microsoft/typescript-go/internal/api/encoder"
	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/astnav"
	"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/json"
	"github.com/microsoft/typescript-go/internal/ls"
	"github.com/microsoft/typescript-go/internal/lsp/lsproto"
	"github.com/microsoft/typescript-go/internal/nodebuilder"
	"github.com/microsoft/typescript-go/internal/pprof"
	"github.com/microsoft/typescript-go/internal/printer"
	"github.com/microsoft/typescript-go/internal/project"
	"github.com/microsoft/typescript-go/internal/tsoptions"
	"github.com/microsoft/typescript-go/internal/tspath"
)

var sessionIDCounter atomic.Uint64

// snapshotData holds the per-snapshot state including the snapshot itself
// and symbol/type registries scoped to this snapshot.
// Multiple clients may hold references to the same snapshot via ref counting;
// the registries are cleaned up when refCount reaches zero.
type snapshotData struct {
	snapshot *project.Snapshot
	refCount int

	// Symbol IDs come from ast.GetSymbolId, a global atomic counter, so the same
	// *ast.Symbol pointer always has the same unique ID across all projects in the
	// snapshot. Symbols are registered snapshot-wide to ensure identity semantics:
	// querying the same symbol from two different projects returns the same handle.
	symbolRegistry   map[SymbolID]*ast.Symbol
	symbolRegistryMu sync.RWMutex

	// symbolCanonicalProjects records, for each registered symbol, the project it was
	// first observed in. Because symbols are shared snapshot-wide (binder symbols are
	// attached to source files, which can be shared across projects), lookups that need
	// a project context (e.g. member/export ordering, node handle resolution) but don't
	// receive one from the caller default to this canonical project. First-writer wins so
	// the choice is stable. Guarded by symbolRegistryMu.
	symbolCanonicalProjects map[SymbolID]ProjectID

	projectRegistries   map[ProjectID]*projectRegistryData
	projectRegistriesMu sync.RWMutex
}

// projectRegistryData holds per-project type and signature registries.
// Types and signatures use per-checker sequential IDs, so the same local ID
// can appear in multiple projects. Separate maps per project prevent collisions
// and allow clean teardown when a project is removed.
type projectRegistryData struct {
	typeRegistry   map[TypeID]*checker.Type
	typeRegistryMu sync.RWMutex

	signatureRegistry   map[SignatureID]*checker.Signature
	signatureRegistryMu sync.RWMutex
}

// getProgram looks up a program from a project handle within this snapshot.
func (sd *snapshotData) getProgram(projectHandle ProjectID) (*compiler.Program, error) {
	proj, err := sd.getProject(projectHandle)
	if err != nil {
		return nil, err
	}

	program := proj.GetProgram()
	if program == nil {
		return nil, fmt.Errorf("%w: project has no program", ErrClientError)
	}

	return program, nil
}

// getProject looks up a project from a project handle within this snapshot.
func (sd *snapshotData) getProject(projectHandle ProjectID) (*project.Project, error) {
	projectName := parseProjectHandle(projectHandle)
	proj := sd.snapshot.ProjectCollection.GetProjectByPath(projectName)
	if proj == nil {
		return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectName)
	}
	return proj, nil
}

// nodeHandleFrom creates an index-based node handle (index.kind.path), building a node index table
// for the file on-demand if needed.
func (sd *snapshotData) nodeHandleFrom(node *ast.Node) NodeHandle {
	sourceFile := ast.GetSourceFileOfNode(node)
	path := sourceFile.Path()
	table := encoder.GetNodeIndexTable(sourceFile)
	idx := table.GetIndex(node)
	return NodeHandle(fmt.Sprintf("%d.%d.%s", idx, node.Kind, path))
}

// getOrCreateProjectRegistry returns the registry for the given project, creating it if needed.
func (sd *snapshotData) getOrCreateProjectRegistry(projectID ProjectID) *projectRegistryData {
	if projectID == "" {
		panic("getOrCreateProjectRegistry: empty project ID")
	}
	// Fast path: registry already exists — read lock only.
	sd.projectRegistriesMu.RLock()
	reg := sd.projectRegistries[projectID]
	sd.projectRegistriesMu.RUnlock()
	if reg != nil {
		return reg
	}
	// Slow path: create under write lock.
	sd.projectRegistriesMu.Lock()
	defer sd.projectRegistriesMu.Unlock()
	if sd.projectRegistries[projectID] == nil {
		sd.projectRegistries[projectID] = &projectRegistryData{
			typeRegistry:      make(map[TypeID]*checker.Type),
			signatureRegistry: make(map[SignatureID]*checker.Signature),
		}
	}
	return sd.projectRegistries[projectID]
}

// newSymbolResponse registers a symbol in the snapshot's registry and returns the response.
// canonicalProject is the project the symbol was observed in and must be non-empty; it is recorded
// as the symbol's canonical project (first writer wins) and returned to the client so it can default
// project-scoped follow-up lookups (members/exports, node resolution) to it.
func (sd *snapshotData) newSymbolResponse(symbol *ast.Symbol, canonicalProject ProjectID) *SymbolResponse {
	if symbol == nil {
		return nil
	}

	id, project := sd.registerSymbol(symbol, canonicalProject)
	resp := &SymbolResponse{
		Id:         id,
		Project:    project,
		Name:       ast.EscapeSymbolName(symbol.Name),
		Flags:      uint32(symbol.Flags),
		CheckFlags: uint32(symbol.CheckFlags),
	}

	if len(symbol.Declarations) > 0 {
		resp.Declarations = make([]NodeHandle, len(symbol.Declarations))
		for i, decl := range symbol.Declarations {
			resp.Declarations[i] = sd.nodeHandleFrom(decl)
		}
	}

	if symbol.ValueDeclaration != nil {
		resp.ValueDeclaration = sd.nodeHandleFrom(symbol.ValueDeclaration)
	}

	if symbol.Parent != nil {
		resp.Parent = SymbolHandle(symbol.Parent)
	}

	if symbol.ExportSymbol != nil {
		resp.ExportSymbol = SymbolHandle(symbol.ExportSymbol)
	}

	return resp
}

// registerSymbol registers a symbol in the snapshot's registry and returns its handle along with
// its canonical project. The canonical project is the project the symbol was first observed in
// (first writer wins for stability) and is always non-empty: every symbol handed to a client must
// carry a project so that project-scoped follow-up lookups (members/exports, parent, node
// resolution) have a default context. Callers must supply a non-empty project.
func (sd *snapshotData) registerSymbol(symbol *ast.Symbol, canonicalProject ProjectID) (SymbolID, ProjectID) {
	if symbol == nil {
		return 0, ""
	}
	if canonicalProject == "" {
		panic("registerSymbol requires a non-empty canonical project")
	}
	id := SymbolHandle(symbol)
	sd.symbolRegistryMu.Lock()
	defer sd.symbolRegistryMu.Unlock()
	existing := sd.symbolRegistry[id]
	if existing != nil {
		if existing != symbol {
			panic("duplicate symbol")
		}
	} else {
		sd.symbolRegistry[id] = symbol
	}
	project, ok := sd.symbolCanonicalProjects[id]
	if !ok {
		sd.symbolCanonicalProjects[id] = canonicalProject
		project = canonicalProject
	}
	return id, project
}

// newTypeResponse registers a type in the project's registry and returns the response.
func (sd *snapshotData) newTypeResponse(projectID ProjectID, t *checker.Type) *TypeResponse {
	if t == nil {
		return nil
	}
	return newTypeResponse(t, sd.registerType(projectID, t))
}

func (sd *snapshotData) registerType(projectID ProjectID, t *checker.Type) TypeID {
	if t == nil {
		return 0
	}
	id := TypeHandle(t)
	reg := sd.getOrCreateProjectRegistry(projectID)
	reg.typeRegistryMu.Lock()
	defer reg.typeRegistryMu.Unlock()
	existing := reg.typeRegistry[id]

	if existing != nil {
		if existing != t {
			panic("duplicate type")
		}
		return id
	}
	reg.typeRegistry[id] = t
	return id
}

// resolveSymbolHandle resolves a symbol handle within the snapshot's registry.
func (sd *snapshotData) resolveSymbolHandle(handle SymbolID) (*ast.Symbol, error) {
	if handle == 0 {
		return nil, fmt.Errorf("%w: empty symbol handle", ErrClientError)
	}

	sd.symbolRegistryMu.RLock()
	symbol, ok := sd.symbolRegistry[handle]
	sd.symbolRegistryMu.RUnlock()

	if !ok {
		return nil, fmt.Errorf("%w: symbol handle %d not found in snapshot registry", ErrClientError, handle)
	}

	return symbol, nil
}

// resolveTypeHandle resolves a type handle within the project's registry.
func (sd *snapshotData) resolveTypeHandle(projectID ProjectID, handle TypeID) (*checker.Type, error) {
	if handle == 0 {
		return nil, fmt.Errorf("%w: empty type handle", ErrClientError)
	}
	if projectID == "" {
		return nil, fmt.Errorf("%w: empty project ID for type handle %d", ErrClientError, handle)
	}

	sd.projectRegistriesMu.RLock()
	reg := sd.projectRegistries[projectID]
	sd.projectRegistriesMu.RUnlock()

	if reg == nil {
		return nil, fmt.Errorf("%w: type handle %d not found (no registry for project %s)", ErrClientError, handle, projectID)
	}

	reg.typeRegistryMu.RLock()
	t, ok := reg.typeRegistry[handle]
	reg.typeRegistryMu.RUnlock()

	if !ok {
		return nil, fmt.Errorf("%w: type handle %d not found in project registry", ErrClientError, handle)
	}

	return t, nil
}

// resolveSignatureHandle resolves a signature handle within the project's registry.
func (sd *snapshotData) resolveSignatureHandle(projectID ProjectID, handle SignatureID) (*checker.Signature, error) {
	if handle == 0 {
		return nil, fmt.Errorf("%w: empty signature handle", ErrClientError)
	}
	if projectID == "" {
		return nil, fmt.Errorf("%w: empty project ID for signature handle %d", ErrClientError, handle)
	}

	sd.projectRegistriesMu.RLock()
	reg := sd.projectRegistries[projectID]
	sd.projectRegistriesMu.RUnlock()

	if reg == nil {
		return nil, fmt.Errorf("%w: signature handle %d not found (no registry for project %s)", ErrClientError, handle, projectID)
	}

	reg.signatureRegistryMu.RLock()
	sig, ok := reg.signatureRegistry[handle]
	reg.signatureRegistryMu.RUnlock()

	if !ok {
		return nil, fmt.Errorf("%w: signature handle %d not found in project registry", ErrClientError, handle)
	}

	return sig, nil
}

// newSignatureResponse registers a signature in the project's registry and returns the response.
func (sd *snapshotData) newSignatureResponse(projectID ProjectID, sig *checker.Signature) *SignatureResponse {
	if sig == nil {
		return nil
	}
	resp := &SignatureResponse{
		Id:    sd.registerSignature(projectID, sig),
		Flags: uint32(sig.Flags()),
	}

	if sig.Declaration() != nil {
		resp.Declaration = sd.nodeHandleFrom(sig.Declaration())
	}

	if len(sig.TypeParameters()) > 0 {
		resp.TypeParameters = typeHandles(sig.TypeParameters())
	}

	if len(sig.Parameters()) > 0 {
		resp.Parameters = symbolHandles(sig.Parameters())
	}

	if sig.ThisParameter() != nil {
		resp.ThisParameter = SymbolHandle(sig.ThisParameter())
	}

	if sig.Target() != nil {
		resp.Target = SignatureHandle(sig.Target())
	}

	return resp
}

func (sd *snapshotData) registerSignature(projectID ProjectID, sig *checker.Signature) SignatureID {
	if sig == nil {
		return 0
	}
	id := SignatureHandle(sig)
	reg := sd.getOrCreateProjectRegistry(projectID)
	reg.signatureRegistryMu.Lock()
	defer reg.signatureRegistryMu.Unlock()
	existing := reg.signatureRegistry[id]

	if existing != nil {
		if existing != sig {
			panic("duplicate signature")
		}
		return id
	}
	reg.signatureRegistry[id] = sig
	return id
}

// Session represents an API session that provides programmatic access
// to TypeScript language services through the LSP server.
// It implements the Handler interface to process incoming API requests.
// The session supports multiple active snapshots, each with their own
// symbol and type registries for maintaining object identity.
type Session struct {
	id             string
	projectSession *project.Session

	// This is set to true when using MessagePackProtocol.
	useBinaryResponses bool

	// snapshots maps snapshot handles to their data. Each snapshot has its own
	// symbol/type registries.
	//
	// snapshotsMu guards the snapshots map and latestSnapshot. It is held only for
	// short, map-bounded critical sections, never across slow work like a project
	// snapshot update or checker queries. Read handlers (getSnapshotData and the
	// language-service handlers built on it) take it for reading; handleRelease and
	// the bookkeeping tail of handleUpdateSnapshot take it for writing. This is what
	// lets queries against an existing snapshot run concurrently with the building of
	// the next one.
	snapshots   map[SnapshotID]*snapshotData
	snapshotsMu sync.RWMutex

	// latestSnapshot tracks the most recently created snapshot, used as the diff base
	// for the next update. Guarded by snapshotsMu.
	latestSnapshot SnapshotID

	// openProjects and openFiles track the projects and files this session
	// currently holds open in the project session's API state. The session holds
	// at most one ref per project/file (opens are idempotent), so it can release
	// exactly those refs on Close and never send a close for a ref it doesn't hold.
	// Guarded by updateMu.
	openProjects collections.Set[tspath.Path]
	openFiles    collections.Set[tspath.Path]

	// updateMu serializes the whole of handleUpdateSnapshot (and releaseOpenRefs)
	// against other updates. Unlike snapshotsMu it is held across the slow
	// projectSession.APIUpdate call, because building the request from
	// openProjects/openFiles, applying it, committing the ref tracking, and advancing
	// latestSnapshot must be one atomic step; otherwise concurrent updates could
	// double-count refs or diff against a non-adjacent snapshot. Read handlers do NOT
	// take this lock, so an in-flight update never blocks queries against existing
	// snapshots. Lock ordering is updateMu -> snapshotsMu (never the reverse).
	updateMu sync.Mutex

	cpuProfiler pprof.CPUProfiler
}

// Ensure Session implements Handler
var _ Handler = (*Session)(nil)

// SessionOptions configures an API session.
type SessionOptions struct {
	// UseBinaryResponses enables binary responses for msgpack protocol.
	UseBinaryResponses bool
}

// NewSession creates a new API session with the given project session.
func NewSession(projectSession *project.Session, options *SessionOptions) *Session {
	id := sessionIDCounter.Add(1)
	s := &Session{
		id:             formatSessionID(id),
		projectSession: projectSession,
		snapshots:      make(map[SnapshotID]*snapshotData),
	}
	if options != nil {
		s.useBinaryResponses = options.UseBinaryResponses
	}
	return s
}

// ID returns the unique identifier for this session.
func (s *Session) ID() string {
	return s.id
}

// ProjectSession returns the underlying project session.
func (s *Session) ProjectSession() *project.Session {
	return s.projectSession
}

// snapshotHandle creates a snapshot handle from a snapshot's ID.
func snapshotHandle(snapshot *project.Snapshot) SnapshotID {
	return SnapshotID(snapshot.ID())
}

// getSnapshotData looks up snapshot data by handle.
func (s *Session) getSnapshotData(handle SnapshotID) (*snapshotData, error) {
	s.snapshotsMu.RLock()
	sd, ok := s.snapshots[handle]
	s.snapshotsMu.RUnlock()
	if !ok {
		return nil, fmt.Errorf("%w: snapshot %d not found", ErrClientError, handle)
	}
	return sd, nil
}

// checkerSetup holds the common context needed by handlers that require a type checker.
type checkerSetup struct {
	sd        *snapshotData
	program   *compiler.Program
	checker   *checker.Checker
	done      func()
	projectID ProjectID
}

func (setup checkerSetup) newTypeResponse(t *checker.Type) *TypeResponse {
	return setup.sd.newTypeResponse(setup.projectID, t)
}

func (setup checkerSetup) newSymbolResponse(sym *ast.Symbol) *SymbolResponse {
	return setup.sd.newSymbolResponse(sym, setup.projectID)
}

func (setup checkerSetup) newSignatureResponse(sig *checker.Signature) *SignatureResponse {
	return setup.sd.newSignatureResponse(setup.projectID, sig)
}

func (setup checkerSetup) resolveTypeHandle(id TypeID) (*checker.Type, error) {
	return setup.sd.resolveTypeHandle(setup.projectID, id)
}

func (setup checkerSetup) resolveSymbolHandle(id SymbolID) (*ast.Symbol, error) {
	return setup.sd.resolveSymbolHandle(id)
}

func (setup checkerSetup) resolveSignatureHandle(id SignatureID) (*checker.Signature, error) {
	return setup.sd.resolveSignatureHandle(setup.projectID, id)
}

// setupChecker resolves snapshot, program, and type checker for a project.
// Callers must defer setup.done() to release the checker.
func (s *Session) setupChecker(ctx context.Context, snapshot SnapshotID, projectHandle ProjectID) (checkerSetup, error) {
	sd, err := s.getSnapshotData(snapshot)
	if err != nil {
		return checkerSetup{}, err
	}

	program, err := sd.getProgram(projectHandle)
	if err != nil {
		return checkerSetup{}, err
	}

	c, done := program.GetTypeChecker(core.WithCheckerLifetime(ctx, core.CheckerLifetimeAPI))
	return checkerSetup{
		sd:        sd,
		program:   program,
		checker:   c,
		done:      done,
		projectID: projectHandle,
	}, nil
}

// setupLanguageService creates a LanguageService for the given snapshot/project.
// Unlike setupChecker, this does NOT acquire a checker from the pool, so callers that
// only need an LS (and not a Checker) can avoid blocking on / holding a pooled checker.
//
// The LS acquires its own checker internally (keyed by the ctx's checker lifetime).
// If a handler returns symbol/type/signature handles the client may later re-query
// on the API checker (e.g. completion with IncludeSymbol -> GetTypeOfSymbol), wrap
// ctx with core.WithCheckerLifetime(ctx, core.CheckerLifetimeAPI) so those handles
// are produced on the persistent API checker and stay resolvable. Only safe when the
// LS operation acquires a checker exactly once; nested acquisitions (e.g. find-all-
// references) would deadlock on the single-slot persistent checker.
func (s *Session) setupLanguageService(sd *snapshotData, program *compiler.Program, projectHandle ProjectID, activeFile string) (*ls.LanguageService, error) {
	projectName := parseProjectHandle(projectHandle)
	proj := sd.snapshot.ProjectCollection.GetProjectByPath(projectName)
	if proj == nil {
		return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectName)
	}
	return ls.NewLanguageService(proj.ID(), program, sd.snapshot, activeFile), nil
}

// HandleRequest implements Handler.
func (s *Session) HandleRequest(ctx context.Context, method string, params json.Value) (any, error) {
	// Handle simple methods that don't need param parsing
	switch method {
	case "echo":
		// Return raw binary for msgpack protocol compatibility
		if s.useBinaryResponses {
			return RawBinary(params), nil
		}
		return params, nil
	case "ping":
		return "pong", nil
	}

	parsed, err := unmarshalPayload(method, params)
	if err != nil {
		return nil, fmt.Errorf("%w: %w", ErrInvalidRequest, err)
	}

	switch method {
	case string(MethodRelease):
		return s.handleRelease(ctx, parsed.(*ReleaseParams))
	case string(MethodInitialize):
		return s.handleInitialize(ctx)
	case string(MethodUpdateSnapshot):
		return s.handleUpdateSnapshot(ctx, parsed.(*UpdateSnapshotParams))
	case string(MethodParseConfigFile):
		return s.handleParseConfigFile(ctx, parsed.(*ParseConfigFileParams))
	case string(MethodGetDefaultProjectForFile):
		return s.handleGetDefaultProjectForFile(ctx, parsed.(*GetDefaultProjectForFileParams))
	case string(MethodGetSourceFile):
		return s.handleGetSourceFile(ctx, parsed.(*GetSourceFileParams))
	case string(MethodGetSourceFileNames):
		return s.handleGetSourceFileNames(ctx, parsed.(*GetSourceFileNamesParams))
	case string(MethodGetSourceFileMetadata):
		return s.handleGetSourceFileMetadata(ctx, parsed.(*GetSourceFileParams))
	case string(MethodGetSymbolAtPosition):
		return s.handleGetSymbolAtPosition(ctx, parsed.(*GetSymbolAtPositionParams))
	case string(MethodGetSymbolsAtPositions):
		return s.handleGetSymbolsAtPositions(ctx, parsed.(*GetSymbolsAtPositionsParams))
	case string(MethodGetSymbolAtLocation):
		return s.handleGetSymbolAtLocation(ctx, parsed.(*GetSymbolAtLocationParams))
	case string(MethodGetSymbolsAtLocations):
		return s.handleGetSymbolsAtLocations(ctx, parsed.(*GetSymbolsAtLocationsParams))
	case string(MethodGetTypeOfSymbol):
		return s.handleGetTypeOfSymbol(ctx, parsed.(*GetTypeOfSymbolParams))
	case string(MethodGetTypesOfSymbols):
		return s.handleGetTypesOfSymbols(ctx, parsed.(*GetTypesOfSymbolsParams))
	case string(MethodGetDeclaredTypeOfSymbol):
		return s.handleGetDeclaredTypeOfSymbol(ctx, parsed.(*GetTypeOfSymbolParams))
	case string(MethodResolveName):
		return s.handleResolveName(ctx, parsed.(*ResolveNameParams))
	case string(MethodGetSignaturesOfType):
		return s.handleGetSignaturesOfType(ctx, parsed.(*GetSignaturesOfTypeParams))
	case string(MethodGetResolvedSignature):
		return s.handleGetResolvedSignature(ctx, parsed.(*GetResolvedSignatureParams))
	case string(MethodGetTypeAtLocation):
		return s.handleGetTypeAtLocation(ctx, parsed.(*GetTypeAtLocationParams))
	case string(MethodGetTypeAtLocations):
		return s.handleGetTypeAtLocations(ctx, parsed.(*GetTypeAtLocationsParams))
	case string(MethodGetTypeAtPosition):
		return s.handleGetTypeAtPosition(ctx, parsed.(*GetTypeAtPositionParams))
	case string(MethodGetTypesAtPositions):
		return s.handleGetTypesAtPositions(ctx, parsed.(*GetTypesAtPositionsParams))
	case string(MethodGetParentOfSymbol):
		return s.handleGetParentOfSymbol(ctx, parsed.(*GetSymbolPropertyParams))
	case string(MethodGetMembersOfSymbol):
		return s.handleGetMembersOfSymbol(ctx, parsed.(*GetSymbolPropertyParams))
	case string(MethodGetExportsOfSymbol):
		return s.handleGetExportsOfSymbol(ctx, parsed.(*GetSymbolPropertyParams))
	case string(MethodGetExportSymbolOfSymbol):
		return s.handleGetExportSymbolOfSymbol(ctx, parsed.(*GetSymbolPropertyParams))
	case string(MethodGetSymbolOfType):
		return s.handleGetSymbolOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetTargetOfType):
		return s.handleGetTargetOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetFreshTypeOfType):
		return s.handleGetFreshTypeOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetRegularTypeOfType):
		return s.handleGetRegularTypeOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetTypesOfType):
		return s.handleGetTypesOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetTypeParametersOfType):
		return s.handleGetTypeParametersOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetOuterTypeParametersOfType):
		return s.handleGetOuterTypeParametersOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetLocalTypeParametersOfType):
		return s.handleGetLocalTypeParametersOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetAliasTypeArgumentsOfType):
		return s.handleGetAliasTypeArgumentsOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetAliasSymbolOfType):
		return s.handleGetAliasSymbolOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetObjectTypeOfType):
		return s.handleGetObjectTypeOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetIndexTypeOfType):
		return s.handleGetIndexTypeOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetCheckTypeOfType):
		return s.handleGetCheckTypeOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetExtendsTypeOfType):
		return s.handleGetExtendsTypeOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetBaseTypeOfType):
		return s.handleGetBaseTypeOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetConstraintOfType):
		return s.handleGetConstraintOfType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetTrueTypeOfConditionalType):
		return s.handleGetTrueTypeOfConditionalType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetFalseTypeOfConditionalType):
		return s.handleGetFalseTypeOfConditionalType(ctx, parsed.(*GetTypePropertyParams))
	case string(MethodGetTypeParametersOfSignature):
		return s.handleGetTypeParametersOfSignature(ctx, parsed.(*GetSignaturePropertyParams))
	case string(MethodGetParametersOfSignature):
		return s.handleGetParametersOfSignature(ctx, parsed.(*GetSignaturePropertyParams))
	case string(MethodGetThisParameterOfSignature):
		return s.handleGetThisParameterOfSignature(ctx, parsed.(*GetSignaturePropertyParams))
	case string(MethodGetTargetOfSignature):
		return s.handleGetTargetOfSignature(ctx, parsed.(*GetSignaturePropertyParams))
	case string(MethodGetContextualType):
		return s.handleGetContextualType(ctx, parsed.(*GetContextualTypeParams))
	case string(MethodGetBaseTypeOfLiteralType):
		return s.handleGetBaseTypeOfLiteralType(ctx, parsed.(*GetBaseTypeOfLiteralTypeParams))
	case string(MethodGetNonNullableType):
		return s.handleGetNonNullableType(ctx, parsed.(*GetNonNullableTypeParams))
	case string(MethodGetTypeFromTypeNode):
		return s.handleGetTypeFromTypeNode(ctx, parsed.(*GetTypeFromTypeNodeParams))
	case string(MethodGetWidenedType):
		return s.handleGetWidenedType(ctx, parsed.(*GetWidenedTypeParams))
	case string(MethodGetParameterType):
		return s.handleGetParameterType(ctx, parsed.(*GetParameterTypeParams))
	case string(MethodIsArrayLikeType):
		return s.handleIsArrayLikeType(ctx, parsed.(*IsArrayLikeTypeParams))
	case string(MethodIsTypeAssignableTo):
		return s.handleIsTypeAssignableTo(ctx, parsed.(*IsTypeAssignableToParams))
	case string(MethodGetShorthandAssignmentValueSymbol):
		return s.handleGetShorthandAssignmentValueSymbol(ctx, parsed.(*GetTypeAtLocationParams))
	case string(MethodGetTypeOfSymbolAtLocation):
		return s.handleGetTypeOfSymbolAtLocation(ctx, parsed.(*GetTypeOfSymbolAtLocationParams))
	case string(MethodTypeToTypeNode):
		return s.handleTypeToTypeNode(ctx, parsed.(*TypeToTypeNodeParams))
	case string(MethodSignatureToSignatureDeclaration):
		return s.handleSignatureToSignatureDeclaration(ctx, parsed.(*SignatureToSignatureDeclarationParams))
	case string(MethodTypeToString):
		return s.handleTypeToString(ctx, parsed.(*TypeToTypeNodeParams))
	case string(MethodPrintNode):
		return s.handlePrintNode(ctx, parsed.(*PrintNodeParams))
	case string(MethodIsContextSensitive):
		return s.handleIsContextSensitive(ctx, parsed.(*GetContextualTypeParams))
	case string(MethodGetReturnTypeOfSignature):
		return s.handleGetReturnTypeOfSignature(ctx, parsed.(*CheckerSignatureParams))
	case string(MethodGetRestTypeOfSignature):
		return s.handleGetRestTypeOfSignature(ctx, parsed.(*CheckerSignatureParams))
	case string(MethodGetTypePredicateOfSignature):
		return s.handleGetTypePredicateOfSignature(ctx, parsed.(*CheckerSignatureParams))
	case string(MethodGetBaseTypes):
		return s.handleGetBaseTypes(ctx, parsed.(*CheckerTypeParams))
	case string(MethodGetPropertiesOfType):
		return s.handleGetPropertiesOfType(ctx, parsed.(*CheckerTypeParams))
	case string(MethodGetApparentType):
		return s.handleGetApparentType(ctx, parsed.(*CheckerTypeParams))
	case string(MethodGetPropertyOfType):
		return s.handleGetPropertyOfType(ctx, parsed.(*GetPropertyOfTypeParams))
	case string(MethodGetIndexInfosOfType):
		return s.handleGetIndexInfosOfType(ctx, parsed.(*CheckerTypeParams))
	case string(MethodGetConstraintOfTypeParameter):
		return s.handleGetConstraintOfTypeParameter(ctx, parsed.(*CheckerTypeParams))
	case string(MethodGetBaseConstraintOfType):
		return s.handleGetBaseConstraintOfType(ctx, parsed.(*CheckerTypeParams))
	case string(MethodGetTypeArguments):
		return s.handleGetTypeArguments(ctx, parsed.(*CheckerTypeParams))
	case string(MethodGetConstantValue):
		return s.handleGetConstantValue(ctx, parsed.(*CheckerNodeParams))
	case string(MethodGetSignatureFromDeclaration):
		return s.handleGetSignatureFromDeclaration(ctx, parsed.(*CheckerNodeParams))
	case string(MethodGetExportSpecifierLocalTarget):
		return s.handleGetExportSpecifierLocalTargetSymbol(ctx, parsed.(*CheckerNodeParams))
	case string(MethodGetAliasedSymbol):
		return s.handleGetAliasedSymbol(ctx, parsed.(*CheckerSymbolParams))
	case string(MethodGetImmediateAliasedSymbol):
		return s.handleGetImmediateAliasedSymbol(ctx, parsed.(*CheckerSymbolParams))
	case string(MethodGetExportsOfModule):
		return s.handleGetExportsOfModule(ctx, parsed.(*CheckerSymbolParams))
	case string(MethodGetMemberInModuleExports):
		return s.handleGetMemberInModuleExports(ctx, parsed.(*GetMemberInModuleExportsParams))
	case string(MethodGetJSDocTags):
		return s.handleGetJSDocTags(ctx, parsed.(*CheckerSymbolParams))
	case string(MethodGetDocumentationComment):
		return s.handleGetDocumentationComment(ctx, parsed.(*CheckerSymbolParams))
	case string(MethodIsArrayType):
		return s.handleIsArrayType(ctx, parsed.(*CheckerTypeParams))
	case string(MethodIsTupleType):
		return s.handleIsTupleType(ctx, parsed.(*CheckerTypeParams))
	case string(MethodGetAnyType):
		return s.handleGetIntrinsicType(ctx, parsed.(*GetIntrinsicTypeParams), (*checker.Checker).GetAnyType)
	case string(MethodGetStringType):
		return s.handleGetIntrinsicType(ctx, parsed.(*GetIntrinsicTypeParams), (*checker.Checker).GetStringType)
	case string(MethodGetNumberType):
		return s.handleGetIntrinsicType(ctx, parsed.(*GetIntrinsicTypeParams), (*checker.Checker).GetNumberType)
	case string(MethodGetBooleanType):
		return s.handleGetIntrinsicType(ctx, parsed.(*GetIntrinsicTypeParams), (*checker.Checker).GetBooleanType)
	case string(MethodGetVoidType):
		return s.handleGetIntrinsicType(ctx, parsed.(*GetIntrinsicTypeParams), (*checker.Checker).GetVoidType)
	case string(MethodGetUndefinedType):
		return s.handleGetIntrinsicType(ctx, parsed.(*GetIntrinsicTypeParams), (*checker.Checker).GetUndefinedType)
	case string(MethodGetNullType):
		return s.handleGetIntrinsicType(ctx, parsed.(*GetIntrinsicTypeParams), (*checker.Checker).GetNullType)
	case string(MethodGetNeverType):
		return s.handleGetIntrinsicType(ctx, parsed.(*GetIntrinsicTypeParams), (*checker.Checker).GetNeverType)
	case string(MethodGetUnknownType):
		return s.handleGetIntrinsicType(ctx, parsed.(*GetIntrinsicTypeParams), (*checker.Checker).GetUnknownType)
	case string(MethodGetBigIntType):
		return s.handleGetIntrinsicType(ctx, parsed.(*GetIntrinsicTypeParams), (*checker.Checker).GetBigIntType)
	case string(MethodGetESSymbolType):
		return s.handleGetIntrinsicType(ctx, parsed.(*GetIntrinsicTypeParams), (*checker.Checker).GetESSymbolType)
	case string(MethodGetWellKnownSymbols):
		return s.handleGetWellKnownSymbols(ctx, parsed.(*GetIntrinsicTypeParams))
	case string(MethodGetWellKnownSignatures):
		return s.handleGetWellKnownSignatures(ctx, parsed.(*GetIntrinsicTypeParams))
	case string(MethodGetSyntacticDiagnostics):
		return s.handleGetSyntacticDiagnostics(ctx, parsed.(*GetDiagnosticsParams))
	case string(MethodGetBindDiagnostics):
		return s.handleGetBindDiagnostics(ctx, parsed.(*GetDiagnosticsParams))
	case string(MethodGetSemanticDiagnostics):
		return s.handleGetSemanticDiagnostics(ctx, parsed.(*GetDiagnosticsParams))
	case string(MethodGetSuggestionDiagnostics):
		return s.handleGetSuggestionDiagnostics(ctx, parsed.(*GetDiagnosticsParams))
	case string(MethodGetDeclarationDiagnostics):
		return s.handleGetDeclarationDiagnostics(ctx, parsed.(*GetDiagnosticsParams))
	case string(MethodGetProgramDiagnostics):
		return s.handleGetProgramDiagnostics(ctx, parsed.(*GetProjectDiagnosticsParams))
	case string(MethodGetGlobalDiagnostics):
		return s.handleGetGlobalDiagnostics(ctx, parsed.(*GetProjectDiagnosticsParams))
	case string(MethodGetConfigFileParsingDiagnostics):
		return s.handleGetConfigFileParsingDiagnostics(ctx, parsed.(*GetProjectDiagnosticsParams))
	case string(MethodStartCPUProfile):
		return s.handleStartCPUProfile(ctx, parsed.(*ProfileParams))
	case string(MethodStopCPUProfile):
		return s.handleStopCPUProfile(ctx)
	case string(MethodSaveHeapProfile):
		return s.handleSaveHeapProfile(ctx, parsed.(*ProfileParams))
	case string(MethodGetReferencesToSymbolInFile):
		return s.handleGetReferencesToSymbolInFile(ctx, parsed.(*GetReferencesToSymbolInFileParams))
	case string(MethodGetReferencedSymbolsForNode):
		return s.handleGetReferencedSymbolsForNode(ctx, parsed.(*GetReferencedSymbolsForNodeParams))
	case string(MethodGetSignatureUsages):
		return s.handleGetSignatureUsages(ctx, parsed.(*GetSignatureUsagesParams))
	case string(MethodGetCompletionsAtPosition):
		return s.handleGetCompletionsAtPosition(ctx, parsed.(*GetCompletionsAtPositionParams))
	default:
		return nil, fmt.Errorf("unknown method: %s", method)
	}
}

func (s *Session) handleStartCPUProfile(_ context.Context, params *ProfileParams) (any, error) {
	if params == nil || params.Dir == "" {
		return nil, fmt.Errorf("%w: dir is required", ErrClientError)
	}
	if err := s.cpuProfiler.StartCPUProfile(params.Dir); err != nil {
		return nil, fmt.Errorf("%w: failed to start CPU profile: %w", ErrClientError, err)
	}
	return nil, nil
}

func (s *Session) handleStopCPUProfile(_ context.Context) (*ProfileResult, error) {
	filePath, err := s.cpuProfiler.StopCPUProfile()
	if err != nil {
		return nil, fmt.Errorf("%w: failed to stop CPU profile: %w", ErrClientError, err)
	}
	return &ProfileResult{File: filePath}, nil
}

func (s *Session) handleSaveHeapProfile(_ context.Context, params *ProfileParams) (*ProfileResult, error) {
	if params == nil || params.Dir == "" {
		return nil, fmt.Errorf("%w: dir is required", ErrClientError)
	}
	filePath, err := pprof.SaveHeapProfile(params.Dir)
	if err != nil {
		return nil, fmt.Errorf("%w: failed to save heap profile: %w", ErrClientError, err)
	}
	return &ProfileResult{File: filePath}, nil
}

// HandleNotification implements Handler.
func (s *Session) HandleNotification(ctx context.Context, method string, params json.Value) error {
	// TODO: Implement notification handling
	return nil
}

func (s *Session) handleInitialize(ctx context.Context) (*InitializeResponse, error) {
	return &InitializeResponse{
		UseCaseSensitiveFileNames: s.projectSession.FS().UseCaseSensitiveFileNames(),
		CurrentDirectory:          s.projectSession.GetCurrentDirectory(),
	}, nil
}

// handleUpdateSnapshot creates a new snapshot, optionally opening or closing
// projects and files. With no args, it adopts the latest LSP state. Opens and
// closes are ref-counted per session: the session holds at most one ref per
// project/file, so repeated opens are idempotent and a close only releases a ref
// the session is actually holding.
func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapshotParams) (*UpdateSnapshotResponse, error) {
	// Fully serialize updates: snapshot creation, ref tracking, and the
	// latestSnapshot/diff bookkeeping must be atomic with respect to other updates,
	// otherwise concurrent updates could compute diffs against a non-adjacent
	// snapshot or leave latestSnapshot pointing at a stale snapshot.
	s.updateMu.Lock()
	defer s.updateMu.Unlock()

	fileChanges := s.toFileChangeSummary(params.FileChanges)

	apiRequest := &project.APISnapshotRequest{}

	// Open projects: only take a new ref for projects we aren't already holding open.
	var openedProjects []tspath.Path
	for _, p := range params.OpenProjects {
		configFileName := p.ToAbsoluteFileName(s.projectSession.GetCurrentDirectory())
		configPath := s.toPath(configFileName)
		if s.openProjects.Has(configPath) {
			continue
		}
		if apiRequest.OpenProjects == nil {
			apiRequest.OpenProjects = collections.NewSetWithSizeHint[string](len(params.OpenProjects))
		}
		apiRequest.OpenProjects.Add(configFileName)
		openedProjects = append(openedProjects, configPath)
	}

	// Close projects: only release a ref we currently hold.
	var closedProjects []tspath.Path
	for _, p := range params.CloseProjects {
		configPath := s.toPath(p.ToAbsoluteFileName(s.projectSession.GetCurrentDirectory()))
		if !s.openProjects.Has(configPath) {
			continue
		}
		if apiRequest.CloseProjects == nil {
			apiRequest.CloseProjects = collections.NewSetWithSizeHint[tspath.Path](len(params.CloseProjects))
		}
		apiRequest.CloseProjects.Add(configPath)
		closedProjects = append(closedProjects, configPath)
	}

	// Open files: only open files we aren't already holding open, so each file is
	// held by at most one API ref from this session.
	var openedFiles []tspath.Path
	for _, f := range params.OpenFiles {
		uri := f.ToURI(s.projectSession.GetCurrentDirectory())
		path := s.toPath(uri.FileName())
		if s.openFiles.Has(path) {
			continue
		}
		if apiRequest.OpenFiles == nil {
			apiRequest.OpenFiles = collections.NewSetWithSizeHint[lsproto.DocumentUri](len(params.OpenFiles))
		}
		apiRequest.OpenFiles.Add(uri)
		openedFiles = append(openedFiles, path)
	}

	// Close files: only release a ref we currently hold.
	var closedFiles []tspath.Path
	for _, f := range params.CloseFiles {
		path := s.toPath(f.ToURI(s.projectSession.GetCurrentDirectory()).FileName())
		if !s.openFiles.Has(path) {
			continue
		}
		if apiRequest.CloseFiles == nil {
			apiRequest.CloseFiles = collections.NewSetWithSizeHint[tspath.Path](len(params.CloseFiles))
		}
		apiRequest.CloseFiles.Add(path)
		closedFiles = append(closedFiles, path)
	}

	// Even when nothing is opened or closed, APIUpdate ensures all projects and
	// files opened by the API are up to date. For an API connected to an LSP server,
	// this brings the API state up to date with the LSP state and ensures projects
	// the API cares about are ready to be queried.
	snapshot, err := s.projectSession.APIUpdate(ctx, fileChanges, apiRequest)
	if err != nil {
		// APIUpdate returns a ref'd snapshot even on error; release it.
		snapshot.Deref(s.projectSession)
		return nil, fmt.Errorf("%w: failed to update snapshot: %w", ErrClientError, err)
	}

	// Commit ref tracking now that the update succeeded.
	for _, configPath := range openedProjects {
		s.openProjects.Add(configPath)
	}
	for _, configPath := range closedProjects {
		s.openProjects.Delete(configPath)
	}
	for _, path := range openedFiles {
		s.openFiles.Add(path)
	}
	for _, path := range closedFiles {
		s.openFiles.Delete(path)
	}

	// Create or ref-count snapshot data, then atomically read the previous latest
	// snapshot (the diff base) and advance latestSnapshot to the new handle.
	// If the same snapshot ID is returned (no changes), we increment the ref count
	// so each client-side Snapshot can be disposed independently.
	handle := snapshotHandle(snapshot)
	s.snapshotsMu.Lock()
	sd, exists := s.snapshots[handle]
	if exists {
		// Same snapshot already stored — release the caller's ref since
		// the stored snapshot already has one, and bump the API refcount.
		snapshot.Deref(s.projectSession)
		sd.refCount++
	} else {
		sd = &snapshotData{
			snapshot:                snapshot,
			refCount:                1,
			symbolRegistry:          make(map[SymbolID]*ast.Symbol),
			symbolCanonicalProjects: make(map[SymbolID]ProjectID),
			projectRegistries:       make(map[ProjectID]*projectRegistryData),
		}
		s.snapshots[handle] = sd
	}
	prevSD := s.snapshots[s.latestSnapshot]
	s.latestSnapshot = handle
	s.snapshotsMu.Unlock()

	// Build projects list
	projects := snapshot.ProjectCollection.Projects()
	projectResponses := make([]*ProjectResponse, 0, len(projects))
	for _, proj := range projects {
		if proj.CommandLine == nil {
			continue
		}
		projectResponses = append(projectResponses, NewProjectResponse(proj))
	}

	// Compute changes from the previous latest snapshot
	var changes *SnapshotChanges
	if prevSD != nil {
		changes = computeSnapshotChanges(prevSD.snapshot, snapshot)
	}

	return &UpdateSnapshotResponse{
		Snapshot: handle,
		Projects: projectResponses,
		Changes:  changes,
	}, nil
}

// handleRelease decrements the ref count for a snapshot.
// The snapshot and its registries are only cleaned up when the ref count reaches zero.
func (s *Session) handleRelease(ctx context.Context, params *ReleaseParams) (any, error) {
	if params == nil || params.Snapshot == 0 {
		return nil, fmt.Errorf("%w: empty handle", ErrClientError)
	}

	s.snapshotsMu.Lock()
	sd := s.snapshots[params.Snapshot]
	if sd == nil {
		s.snapshotsMu.Unlock()
		return nil, fmt.Errorf("%w: snapshot %d not found", ErrClientError, params.Snapshot)
	}
	sd.refCount--
	if sd.refCount <= 0 {
		delete(s.snapshots, params.Snapshot)
		// Release the API session's ref on the project snapshot.
		sd.snapshot.Deref(s.projectSession)
	}
	s.snapshotsMu.Unlock()
	return true, nil
}

// handleGetDefaultProjectForFile returns the default project for a given file,
// or nil if no project currently contains the file.
func (s *Session) handleGetDefaultProjectForFile(ctx context.Context, params *GetDefaultProjectForFileParams) (*ProjectResponse, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	uri := params.File.ToURI(s.projectSession.GetCurrentDirectory())
	proj := sd.snapshot.GetDefaultProject(uri)
	if proj == nil {
		return nil, nil
	}

	return NewProjectResponse(proj), nil
}

// handleParseConfigFile parses a tsconfig.json file and returns its contents.
func (s *Session) handleParseConfigFile(ctx context.Context, params *ParseConfigFileParams) (*ConfigFileResponse, error) {
	configFileName := params.File.ToAbsoluteFileName(s.projectSession.GetCurrentDirectory())
	configFileContent, ok := s.projectSession.FS().ReadFile(configFileName)
	if !ok {
		return nil, fmt.Errorf("%w: could not read file %q", ErrClientError, configFileName)
	}

	configDir := tspath.GetDirectoryPath(configFileName)
	tsConfigSourceFile := tsoptions.NewTsconfigSourceFileFromFilePath(
		configFileName,
		s.toPath(configFileName),
		configFileContent,
	)
	parsedCommandLine := tsoptions.ParseJsonSourceFileConfigFileContent(
		tsConfigSourceFile,
		s.projectSession,
		configDir,
		nil, /*existingOptions*/
		nil, /*existingOptionsRaw*/
		configFileName,
		nil, /*resolutionStack*/
		nil, /*extraFileExtensions*/
		nil, /*extendedConfigCache*/
	)

	return &ConfigFileResponse{
		FileNames: parsedCommandLine.FileNames(),
		Options:   parsedCommandLine.CompilerOptions(),
	}, nil
}

// handleGetSourceFile returns a source file from a project within a snapshot.
func (s *Session) handleGetSourceFile(ctx context.Context, params *GetSourceFileParams) (any, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	program, err := sd.getProgram(params.Project)
	if err != nil {
		return nil, err
	}

	sourceFile := program.GetSourceFile(params.File.ToFileName())
	if sourceFile == nil {
		if s.useBinaryResponses {
			return RawBinary(nil), nil
		}
		return nil, nil
	}

	// Encode the full source file.
	data, _, err := encoder.EncodeSourceFile(sourceFile)
	if err != nil {
		return nil, fmt.Errorf("failed to encode source file: %w", err)
	}

	// Return raw binary for msgpack protocol, or base64 for JSON
	if s.useBinaryResponses {
		return RawBinary(data), nil
	}
	return &SourceFileResponse{
		Data: base64.StdEncoding.EncodeToString(data),
	}, nil
}

// handleGetSourceFileNames returns file names of all source files in a project.
func (s *Session) handleGetSourceFileNames(ctx context.Context, params *GetSourceFileNamesParams) ([]string, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	program, err := sd.getProgram(params.Project)
	if err != nil {
		return nil, err
	}

	sourceFiles := program.GetSourceFiles()
	result := make([]string, len(sourceFiles))
	for i, sourceFile := range sourceFiles {
		result[i] = sourceFile.FileName()
	}
	return result, nil
}

// handleGetSourceFileMetadata returns program-stored metadata for a single source file.
// The client fetches this lazily per file and caches it.
func (s *Session) handleGetSourceFileMetadata(ctx context.Context, params *GetSourceFileParams) (*SourceFileMetadata, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	program, err := sd.getProgram(params.Project)
	if err != nil {
		return nil, err
	}

	sourceFile := program.GetSourceFile(params.File.ToFileName())
	if sourceFile == nil {
		return nil, nil
	}

	metaData := program.GetSourceFileMetaData(sourceFile.Path())
	return &SourceFileMetadata{
		IsDefaultLibrary:      program.IsSourceFileDefaultLibrary(sourceFile.Path()),
		IsFromExternalLibrary: program.IsSourceFileFromExternalLibrary(sourceFile),
		PackageJsonType:       metaData.PackageJsonType,
		PackageJsonDirectory:  metaData.PackageJsonDirectory,
		ImpliedNodeFormat:     metaData.ImpliedNodeFormat,
	}, nil
}

// handleGetSymbolAtPosition returns the symbol at a position in a file.
func (s *Session) handleGetSymbolAtPosition(ctx context.Context, params *GetSymbolAtPositionParams) (*SymbolResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	sourceFile := setup.program.GetSourceFile(params.File.ToFileName())
	if sourceFile == nil {
		return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File)
	}

	positionMap := sourceFile.GetPositionMap()
	node := astnav.GetTouchingPropertyName(sourceFile, positionMap.UTF16ToUTF8(int(params.Position)))
	if node == nil {
		return nil, nil
	}

	symbol := setup.checker.GetSymbolAtLocation(node)
	if symbol == nil {
		return nil, nil
	}

	return setup.newSymbolResponse(symbol), nil
}

// handleGetSymbolsAtPositions returns symbols at multiple positions in a file.
func (s *Session) handleGetSymbolsAtPositions(ctx context.Context, params *GetSymbolsAtPositionsParams) ([]*SymbolResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	sourceFile := setup.program.GetSourceFile(params.File.ToFileName())
	if sourceFile == nil {
		return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File)
	}

	positionMap := sourceFile.GetPositionMap()
	results := make([]*SymbolResponse, len(params.Positions))
	for i, pos := range params.Positions {
		node := astnav.GetTouchingPropertyName(sourceFile, positionMap.UTF16ToUTF8(int(pos)))
		if node == nil {
			continue
		}
		symbol := setup.checker.GetSymbolAtLocation(node)
		if symbol != nil {
			results[i] = setup.newSymbolResponse(symbol)
		}
	}

	return results, nil
}

// handleGetSymbolAtLocation returns the symbol at a node location.
func (s *Session) handleGetSymbolAtLocation(ctx context.Context, params *GetSymbolAtLocationParams) (*SymbolResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	node, err := setup.sd.resolveNodeHandle(setup.program, params.Location)
	if err != nil {
		return nil, err
	}
	if node == nil {
		return nil, nil
	}

	symbol := setup.checker.GetSymbolAtLocation(node)
	if symbol == nil {
		return nil, nil
	}

	return setup.newSymbolResponse(symbol), nil
}

// handleGetSymbolsAtLocations returns symbols at multiple node locations.
func (s *Session) handleGetSymbolsAtLocations(ctx context.Context, params *GetSymbolsAtLocationsParams) ([]*SymbolResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	results := make([]*SymbolResponse, len(params.Locations))
	for i, loc := range params.Locations {
		node, err := setup.sd.resolveNodeHandle(setup.program, loc)
		if err != nil {
			return nil, err
		}
		if node == nil {
			continue
		}
		symbol := setup.checker.GetSymbolAtLocation(node)
		if symbol != nil {
			results[i] = setup.newSymbolResponse(symbol)
		}
	}

	return results, nil
}

// handleGetTypeOfSymbol returns the type of a symbol.
func (s *Session) handleGetTypeOfSymbol(ctx context.Context, params *GetTypeOfSymbolParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	symbol, err := setup.resolveSymbolHandle(params.Symbol)
	if err != nil {
		return nil, err
	}

	return setup.newTypeResponse(setup.checker.GetTypeOfSymbol(symbol)), nil
}

// handleGetTypesOfSymbols returns the types of multiple symbols.
func (s *Session) handleGetTypesOfSymbols(ctx context.Context, params *GetTypesOfSymbolsParams) ([]*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	results := make([]*TypeResponse, len(params.Symbols))
	for i, symHandle := range params.Symbols {
		symbol, err := setup.resolveSymbolHandle(symHandle)
		if err != nil {
			return nil, err
		}
		// resolveSymbolHandle errors on an unresolvable handle and GetTypeOfSymbol
		// never returns nil, so every element resolves to a type (error type at worst).
		results[i] = setup.newTypeResponse(setup.checker.GetTypeOfSymbol(symbol))
	}

	return results, nil
}

// handleGetDeclaredTypeOfSymbol returns the declared type of a symbol (e.g. the type alias body for type alias symbols).
func (s *Session) handleGetDeclaredTypeOfSymbol(ctx context.Context, params *GetTypeOfSymbolParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	symbol, err := setup.resolveSymbolHandle(params.Symbol)
	if err != nil {
		return nil, err
	}

	return setup.newTypeResponse(setup.checker.GetDeclaredTypeOfSymbol(symbol)), nil
}

// handleResolveName resolves a name to a symbol at a given location.
func (s *Session) handleResolveName(ctx context.Context, params *ResolveNameParams) (*SymbolResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	// Resolve location node - either from node handle or from fileName+position
	var location *ast.Node
	if params.Location != "" {
		location, err = setup.sd.resolveNodeHandle(setup.program, params.Location)
		if err != nil {
			return nil, err
		}
	} else if params.File != nil && params.Position != nil {
		sourceFile := setup.program.GetSourceFile(params.File.ToFileName())
		if sourceFile == nil {
			return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, *params.File)
		}
		location = astnav.GetTouchingPropertyName(sourceFile, sourceFile.GetPositionMap().UTF16ToUTF8(int(*params.Position)))
	}

	symbol := setup.checker.ResolveName(params.Name, location, ast.SymbolFlags(params.Meaning), params.ExcludeGlobals)
	if symbol == nil {
		return nil, nil
	}

	return setup.newSymbolResponse(symbol), nil
}

// handleGetSignaturesOfType returns the call or construct signatures of a type.
func (s *Session) handleGetSignaturesOfType(ctx context.Context, params *GetSignaturesOfTypeParams) ([]*SignatureResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return nil, err
	}

	sigs := setup.checker.GetSignaturesOfType(t, checker.SignatureKind(params.Kind))
	results := make([]*SignatureResponse, len(sigs))
	for i, sig := range sigs {
		results[i] = setup.newSignatureResponse(sig)
	}

	return results, nil
}

// handleGetResolvedSignature returns the resolved signature of a call-like expression.
func (s *Session) handleGetResolvedSignature(ctx context.Context, params *GetResolvedSignatureParams) (*SignatureResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	node, err := setup.sd.resolveNodeHandle(setup.program, params.Location)
	if err != nil {
		return nil, err
	}

	return setup.newSignatureResponse(setup.checker.GetResolvedSignature(node)), nil
}

// handleGetTypeAtLocation returns the type at a node location.
func (s *Session) handleGetTypeAtLocation(ctx context.Context, params *GetTypeAtLocationParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	node, err := setup.sd.resolveNodeHandle(setup.program, params.Location)
	if err != nil {
		return nil, err
	}

	return setup.newTypeResponse(setup.checker.GetTypeAtLocation(node)), nil
}

// handleGetTypeAtLocations returns types at multiple node locations.
func (s *Session) handleGetTypeAtLocations(ctx context.Context, params *GetTypeAtLocationsParams) ([]*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	results := make([]*TypeResponse, len(params.Locations))
	for i, loc := range params.Locations {
		node, err := setup.sd.resolveNodeHandle(setup.program, loc)
		if err != nil {
			return nil, err
		}
		// resolveNodeHandle errors on an unresolvable handle and GetTypeAtLocation
		// never returns nil, so every element resolves to a type (error type at worst).
		results[i] = setup.newTypeResponse(setup.checker.GetTypeAtLocation(node))
	}

	return results, nil
}

// handleGetTypeAtPosition returns the type at a position in a file.
func (s *Session) handleGetTypeAtPosition(ctx context.Context, params *GetTypeAtPositionParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	sourceFile := setup.program.GetSourceFile(params.File.ToFileName())
	if sourceFile == nil {
		return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File)
	}

	positionMap := sourceFile.GetPositionMap()
	node := astnav.GetTouchingPropertyName(sourceFile, positionMap.UTF16ToUTF8(int(params.Position)))
	if node == nil {
		return nil, nil
	}

	t := setup.checker.GetTypeAtLocation(node)
	if t == nil {
		return nil, nil
	}

	return setup.newTypeResponse(t), nil
}

// handleGetTypesAtPositions returns types at multiple positions in a file.
func (s *Session) handleGetTypesAtPositions(ctx context.Context, params *GetTypesAtPositionsParams) ([]*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	sourceFile := setup.program.GetSourceFile(params.File.ToFileName())
	if sourceFile == nil {
		return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File)
	}

	positionMap := sourceFile.GetPositionMap()
	results := make([]*TypeResponse, len(params.Positions))
	for i, pos := range params.Positions {
		node := astnav.GetTouchingPropertyName(sourceFile, positionMap.UTF16ToUTF8(int(pos)))
		if node == nil {
			continue
		}
		t := setup.checker.GetTypeAtLocation(node)
		if t != nil {
			results[i] = setup.newTypeResponse(t)
		}
	}

	return results, nil
}

func (s *Session) handleGetParentOfSymbol(_ context.Context, params *GetSymbolPropertyParams) (*SymbolResponse, error) {
	return s.resolveSymbolPropertyOfSymbol(params, func(sym *ast.Symbol) *ast.Symbol { return sym.Parent })
}

func (s *Session) handleGetMembersOfSymbol(ctx context.Context, params *GetSymbolPropertyParams) ([]*SymbolResponse, error) {
	return s.resolveSymbolTablePropertyOfSymbol(ctx, params, func(symbol *ast.Symbol) ast.SymbolTable {
		return symbol.Members
	})
}

func (s *Session) handleGetExportsOfSymbol(ctx context.Context, params *GetSymbolPropertyParams) ([]*SymbolResponse, error) {
	return s.resolveSymbolTablePropertyOfSymbol(ctx, params, func(symbol *ast.Symbol) ast.SymbolTable {
		return symbol.Exports
	})
}

func (s *Session) handleGetExportSymbolOfSymbol(_ context.Context, params *GetSymbolPropertyParams) (*SymbolResponse, error) {
	return s.resolveSymbolPropertyOfSymbol(params, func(sym *ast.Symbol) *ast.Symbol { return sym.ExportSymbol })
}

func (s *Session) handleGetSymbolOfType(_ context.Context, params *GetTypePropertyParams) (*SymbolResponse, error) {
	return s.resolveSymbolPropertyOfType(params, (*checker.Type).Symbol)
}

func (s *Session) handleGetTargetOfType(_ context.Context, params *GetTypePropertyParams) (*TypeResponse, error) {
	return s.resolveTypePropertyOfType(params, (*checker.Type).Target)
}

func (s *Session) handleGetFreshTypeOfType(_ context.Context, params *GetTypePropertyParams) (*TypeResponse, error) {
	return s.resolveTypePropertyOfType(params, func(t *checker.Type) *checker.Type { return t.AsLiteralType().FreshType() })
}

func (s *Session) handleGetRegularTypeOfType(_ context.Context, params *GetTypePropertyParams) (*TypeResponse, error) {
	return s.resolveTypePropertyOfType(params, func(t *checker.Type) *checker.Type { return t.AsLiteralType().RegularType() })
}

func (s *Session) handleGetTypesOfType(_ context.Context, params *GetTypePropertyParams) ([]*TypeResponse, error) {
	return s.resolveTypeArrayPropertyOfType(params, (*checker.Type).Types)
}

func (s *Session) handleGetTypeParametersOfType(_ context.Context, params *GetTypePropertyParams) ([]*TypeResponse, error) {
	return s.resolveTypeArrayPropertyOfType(params, func(t *checker.Type) []*checker.Type { return t.AsInterfaceType().TypeParameters() })
}

func (s *Session) handleGetOuterTypeParametersOfType(_ context.Context, params *GetTypePropertyParams) ([]*TypeResponse, error) {
	return s.resolveTypeArrayPropertyOfType(params, func(t *checker.Type) []*checker.Type { return t.AsInterfaceType().OuterTypeParameters() })
}

func (s *Session) handleGetLocalTypeParametersOfType(_ context.Context, params *GetTypePropertyParams) ([]*TypeResponse, error) {
	return s.resolveTypeArrayPropertyOfType(params, func(t *checker.Type) []*checker.Type { return t.AsInterfaceType().LocalTypeParameters() })
}

func (s *Session) handleGetAliasTypeArgumentsOfType(_ context.Context, params *GetTypePropertyParams) ([]*TypeResponse, error) {
	return s.resolveTypeArrayPropertyOfType(params, func(t *checker.Type) []*checker.Type {
		if t.Alias() == nil {
			return nil
		}
		return t.Alias().TypeArguments()
	})
}

func (s *Session) handleGetAliasSymbolOfType(_ context.Context, params *GetTypePropertyParams) (*SymbolResponse, error) {
	return s.resolveSymbolPropertyOfType(params, func(t *checker.Type) *ast.Symbol {
		if t.Alias() == nil {
			return nil
		}
		return t.Alias().Symbol()
	})
}

func (s *Session) handleGetObjectTypeOfType(_ context.Context, params *GetTypePropertyParams) (*TypeResponse, error) {
	return s.resolveTypePropertyOfType(params, func(t *checker.Type) *checker.Type { return t.AsIndexedAccessType().ObjectType() })
}

func (s *Session) handleGetIndexTypeOfType(_ context.Context, params *GetTypePropertyParams) (*TypeResponse, error) {
	return s.resolveTypePropertyOfType(params, func(t *checker.Type) *checker.Type { return t.AsIndexedAccessType().IndexType() })
}

func (s *Session) handleGetCheckTypeOfType(_ context.Context, params *GetTypePropertyParams) (*TypeResponse, error) {
	return s.resolveTypePropertyOfType(params, func(t *checker.Type) *checker.Type { return t.AsConditionalType().CheckType() })
}

func (s *Session) handleGetExtendsTypeOfType(_ context.Context, params *GetTypePropertyParams) (*TypeResponse, error) {
	return s.resolveTypePropertyOfType(params, func(t *checker.Type) *checker.Type { return t.AsConditionalType().ExtendsType() })
}

func (s *Session) handleGetBaseTypeOfType(_ context.Context, params *GetTypePropertyParams) (*TypeResponse, error) {
	return s.resolveTypePropertyOfType(params, func(t *checker.Type) *checker.Type { return t.AsSubstitutionType().BaseType() })
}

func (s *Session) handleGetConstraintOfType(_ context.Context, params *GetTypePropertyParams) (*TypeResponse, error) {
	return s.resolveTypePropertyOfType(params, func(t *checker.Type) *checker.Type { return t.AsSubstitutionType().SubstConstraint() })
}

func (s *Session) handleGetTypeParametersOfSignature(_ context.Context, params *GetSignaturePropertyParams) ([]*TypeResponse, error) {
	return s.resolveTypeArrayPropertyOfSignature(params, (*checker.Signature).TypeParameters)
}

func (s *Session) handleGetParametersOfSignature(_ context.Context, params *GetSignaturePropertyParams) ([]*SymbolResponse, error) {
	return s.resolveSymbolArrayPropertyOfSignature(params, (*checker.Signature).Parameters)
}

func (s *Session) handleGetThisParameterOfSignature(_ context.Context, params *GetSignaturePropertyParams) (*SymbolResponse, error) {
	return s.resolveSymbolPropertyOfSignature(params, (*checker.Signature).ThisParameter)
}

func (s *Session) handleGetTargetOfSignature(_ context.Context, params *GetSignaturePropertyParams) (*SignatureResponse, error) {
	return s.resolveSignaturePropertyOfSignature(params, (*checker.Signature).Target)
}

// resolveTypePropertyOfType resolves a type property of type `Type` and returns a type response.
func (s *Session) resolveTypePropertyOfType(params *GetTypePropertyParams, getter func(*checker.Type) *checker.Type) (*TypeResponse, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	t, err := sd.resolveTypeHandle(params.Project, params.Type)
	if err != nil {
		return nil, err
	}

	result := getter(t)
	if result == nil {
		return nil, nil
	}

	return sd.newTypeResponse(params.Project, result), nil
}

// resolveTypeArrayPropertyOfType resolves a type property of an array of types and returns an array of type responses.
func (s *Session) resolveTypeArrayPropertyOfType(params *GetTypePropertyParams, getter func(*checker.Type) []*checker.Type) ([]*TypeResponse, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	t, err := sd.resolveTypeHandle(params.Project, params.Type)
	if err != nil {
		return nil, err
	}

	types := getter(t)
	if len(types) == 0 {
		return nil, nil
	}

	results := make([]*TypeResponse, len(types))
	for i, sub := range types {
		results[i] = sd.newTypeResponse(params.Project, sub)
	}
	return results, nil
}

// resolveSymbolPropertyOfType resolves a type property of type `Symbol` and returns a symbol response.
func (s *Session) resolveSymbolPropertyOfType(params *GetTypePropertyParams, getter func(*checker.Type) *ast.Symbol) (*SymbolResponse, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	t, err := sd.resolveTypeHandle(params.Project, params.Type)
	if err != nil {
		return nil, err
	}

	result := getter(t)
	if result == nil {
		return nil, nil
	}
	return sd.newSymbolResponse(result, params.Project), nil
}

// resolveSymbolTablePropertyOfSymbol resolves a symbol property of type `Symbol` and returns a symbol response.
func (s *Session) resolveSymbolPropertyOfSymbol(params *GetSymbolPropertyParams, getter func(*ast.Symbol) *ast.Symbol) (*SymbolResponse, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	symbol, err := sd.resolveSymbolHandle(params.Symbol)
	if err != nil {
		return nil, err
	}

	result := getter(symbol)
	if result == nil {
		return nil, nil
	}
	return sd.newSymbolResponse(result, params.Project), nil
}

// resolveSymbolTablePropertyOfSymbol resolves a symbol property of type `SymbolTable` and returns an array of symbol responses.
// Results are sorted using the checker's canonical symbol ordering so that API consumers receive
// a stable, deterministic order instead of Go's randomized map iteration order.
func (s *Session) resolveSymbolTablePropertyOfSymbol(ctx context.Context, params *GetSymbolPropertyParams, getter func(*ast.Symbol) ast.SymbolTable) ([]*SymbolResponse, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	symbol, err := sd.resolveSymbolHandle(params.Symbol)
	if err != nil {
		return nil, err
	}

	symbolTable := getter(symbol)
	if len(symbolTable) == 0 {
		return nil, nil
	}
	if len(symbolTable) == 1 {
		for _, sub := range symbolTable {
			return []*SymbolResponse{sd.newSymbolResponse(sub, params.Project)}, nil
		}
	}

	// More than one symbol, need a checker to sort
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	symbols := make([]*ast.Symbol, 0, len(symbolTable))
	for _, sub := range symbolTable {
		symbols = append(symbols, sub)
	}
	slices.SortFunc(symbols, setup.checker.CompareSymbols)

	results := make([]*SymbolResponse, len(symbols))
	for i, sub := range symbols {
		results[i] = setup.newSymbolResponse(sub)
	}
	return results, nil
}

// resolveSymbolArrayPropertyOfSignature resolves a signature property of an array of symbols and returns an array of symbol responses.
func (s *Session) resolveSymbolArrayPropertyOfSignature(params *GetSignaturePropertyParams, getter func(*checker.Signature) []*ast.Symbol) ([]*SymbolResponse, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	sig, err := sd.resolveSignatureHandle(params.Project, params.Signature)
	if err != nil {
		return nil, err
	}

	symbols := getter(sig)
	if len(symbols) == 0 {
		return nil, nil
	}

	results := make([]*SymbolResponse, len(symbols))
	for i, sym := range symbols {
		results[i] = sd.newSymbolResponse(sym, params.Project)
	}
	return results, nil
}

// resolveSymbolPropertyOfSignature resolves a signature property of type `Symbol` and returns a symbol response.
func (s *Session) resolveSymbolPropertyOfSignature(params *GetSignaturePropertyParams, getter func(*checker.Signature) *ast.Symbol) (*SymbolResponse, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	sig, err := sd.resolveSignatureHandle(params.Project, params.Signature)
	if err != nil {
		return nil, err
	}

	result := getter(sig)
	if result == nil {
		return nil, nil
	}
	return sd.newSymbolResponse(result, params.Project), nil
}

func (s *Session) resolveTypeArrayPropertyOfSignature(params *GetSignaturePropertyParams, getter func(signature *checker.Signature) []*checker.Type) ([]*TypeResponse, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	sig, err := sd.resolveSignatureHandle(params.Project, params.Signature)
	if err != nil {
		return nil, err
	}

	types := getter(sig)
	if len(types) == 0 {
		return nil, nil
	}

	results := make([]*TypeResponse, len(types))
	for i, sub := range types {
		results[i] = sd.newTypeResponse(params.Project, sub)
	}
	return results, nil
}

func (s *Session) resolveSignaturePropertyOfSignature(params *GetSignaturePropertyParams, getter func(*checker.Signature) *checker.Signature) (*SignatureResponse, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	sig, err := sd.resolveSignatureHandle(params.Project, params.Signature)
	if err != nil {
		return nil, err
	}

	result := getter(sig)
	if result == nil {
		return nil, nil
	}
	return sd.newSignatureResponse(params.Project, result), nil
}

// handleGetContextualType returns the contextual type for a node.
func (s *Session) handleGetContextualType(ctx context.Context, params *GetContextualTypeParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	node, err := setup.sd.resolveNodeHandle(setup.program, params.Location)
	if err != nil {
		return nil, err
	}
	if node == nil {
		return nil, nil
	}

	t := setup.checker.GetContextualType(node, checker.ContextFlagsNone)
	if t == nil {
		return nil, nil
	}

	return setup.newTypeResponse(t), nil
}

// handleGetBaseTypeOfLiteralType returns the base type of a literal type (e.g. number for 42).
func (s *Session) handleGetBaseTypeOfLiteralType(ctx context.Context, params *GetBaseTypeOfLiteralTypeParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return nil, err
	}

	return setup.newTypeResponse(setup.checker.GetBaseTypeOfLiteralType(t)), nil
}

// handleGetNonNullableType returns the type with null and undefined removed.
func (s *Session) handleGetNonNullableType(ctx context.Context, params *GetNonNullableTypeParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return nil, err
	}

	return setup.newTypeResponse(setup.checker.GetNonNullableType(t)), nil
}

// handleGetTypeFromTypeNode returns the type for a type node.
func (s *Session) handleGetTypeFromTypeNode(ctx context.Context, params *GetTypeFromTypeNodeParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	node, err := setup.sd.resolveNodeHandle(setup.program, params.Location)
	if err != nil {
		return nil, err
	}

	return setup.newTypeResponse(setup.checker.GetTypeFromTypeNode(node)), nil
}

// handleGetWidenedType returns the widened type.
func (s *Session) handleGetWidenedType(ctx context.Context, params *GetWidenedTypeParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return nil, err
	}

	return setup.newTypeResponse(setup.checker.GetWidenedType(t)), nil
}

// handleGetParameterType returns the type of a parameter at a given index in a signature.
func (s *Session) handleGetParameterType(ctx context.Context, params *GetParameterTypeParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	sig, err := setup.resolveSignatureHandle(params.Signature)
	if err != nil {
		return nil, err
	}

	if params.Index < 0 {
		return nil, fmt.Errorf("%w: invalid parameter index", ErrClientError)
	}

	return setup.newTypeResponse(setup.checker.GetTypeAtPosition(sig, int(params.Index))), nil
}

// handleIsArrayLikeType returns whether a type is array-like.
func (s *Session) handleIsArrayLikeType(ctx context.Context, params *IsArrayLikeTypeParams) (bool, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return false, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return false, err
	}

	return setup.checker.IsArrayLikeType(t), nil
}

// handleIsTypeAssignableTo returns whether source is assignable to target.
func (s *Session) handleIsTypeAssignableTo(ctx context.Context, params *IsTypeAssignableToParams) (bool, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return false, err
	}
	defer setup.done()

	source, err := setup.resolveTypeHandle(params.Source)
	if err != nil {
		return false, err
	}
	target, err := setup.resolveTypeHandle(params.Target)
	if err != nil {
		return false, err
	}

	return setup.checker.IsTypeAssignableTo(source, target), nil
}

// handleGetShorthandAssignmentValueSymbol returns the value symbol of a shorthand property assignment.
func (s *Session) handleGetShorthandAssignmentValueSymbol(ctx context.Context, params *GetTypeAtLocationParams) (*SymbolResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	node, err := setup.sd.resolveNodeHandle(setup.program, params.Location)
	if err != nil {
		return nil, err
	}
	if node == nil {
		return nil, nil
	}

	symbol := setup.checker.GetShorthandAssignmentValueSymbol(node)
	if symbol == nil {
		return nil, nil
	}

	return setup.newSymbolResponse(symbol), nil
}

// handleGetTypeOfSymbolAtLocation returns the narrowed type of a symbol at a specific location.
func (s *Session) handleGetTypeOfSymbolAtLocation(ctx context.Context, params *GetTypeOfSymbolAtLocationParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	symbol, err := setup.resolveSymbolHandle(params.Symbol)
	if err != nil {
		return nil, err
	}

	node, err := setup.sd.resolveNodeHandle(setup.program, params.Location)
	if err != nil {
		return nil, err
	}

	return setup.newTypeResponse(setup.checker.GetTypeOfSymbolAtLocation(symbol, node)), nil
}

// handleTypeToTypeNode converts a Type to a TypeNode AST and returns it as binary-encoded data.
func (s *Session) handleTypeToTypeNode(ctx context.Context, params *TypeToTypeNodeParams) (any, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return nil, err
	}

	var enclosingDeclaration *ast.Node
	if params.Location != "" {
		enclosingDeclaration, err = setup.sd.resolveNodeHandle(setup.program, params.Location)
		if err != nil {
			return nil, err
		}
	}

	typeNode := setup.checker.TypeToTypeNode(t, enclosingDeclaration, nodebuilder.Flags(params.Flags), nil)
	if typeNode == nil {
		return nil, nil
	}

	data, _, err := encoder.EncodeNode(typeNode.AsNode(), nil)
	if err != nil {
		return nil, fmt.Errorf("failed to encode type node: %w", err)
	}

	if s.useBinaryResponses {
		return RawBinary(data), nil
	}
	return &SourceFileResponse{
		Data: base64.StdEncoding.EncodeToString(data),
	}, nil
}

func (s *Session) handleSignatureToSignatureDeclaration(ctx context.Context, params *SignatureToSignatureDeclarationParams) (any, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	sig, err := setup.resolveSignatureHandle(params.Signature)
	if err != nil {
		return nil, err
	}

	var enclosingDeclaration *ast.Node
	if params.Location != "" {
		enclosingDeclaration, err = setup.sd.resolveNodeHandle(setup.program, params.Location)
		if err != nil {
			return nil, err
		}
	}

	node := setup.checker.SignatureToSignatureDeclaration(sig, ast.Kind(params.Kind), enclosingDeclaration, nodebuilder.Flags(params.Flags))
	if node == nil {
		return nil, nil
	}

	data, _, err := encoder.EncodeNode(node.AsNode(), nil)
	if err != nil {
		return nil, fmt.Errorf("failed to encode signature declaration: %w", err)
	}

	if s.useBinaryResponses {
		return RawBinary(data), nil
	}
	return &SourceFileResponse{
		Data: base64.StdEncoding.EncodeToString(data),
	}, nil
}

// handleTypeToString converts a Type to its string representation.
func (s *Session) handleTypeToString(ctx context.Context, params *TypeToTypeNodeParams) (any, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return nil, err
	}

	var enclosingDeclaration *ast.Node
	if params.Location != "" {
		enclosingDeclaration, err = setup.sd.resolveNodeHandle(setup.program, params.Location)
		if err != nil {
			return nil, err
		}
	}

	if params.Flags != 0 {
		return setup.checker.TypeToStringEx(t, enclosingDeclaration, checker.TypeFormatFlags(params.Flags), nil), nil
	}
	return setup.checker.TypeToStringEx(t, enclosingDeclaration, checker.TypeFormatFlagsAllowUniqueESSymbolType|checker.TypeFormatFlagsUseAliasDefinedOutsideCurrentScope, nil), nil
}

// handlePrintNode decodes a binary-encoded AST node and prints it to text.
func (s *Session) handlePrintNode(_ context.Context, params *PrintNodeParams) (string, error) {
	data, err := base64.StdEncoding.DecodeString(params.Data)
	if err != nil {
		return "", fmt.Errorf("%w: invalid base64 data: %w", ErrClientError, err)
	}

	node, err := encoder.DecodeNodes(data)
	if err != nil {
		return "", fmt.Errorf("%w: failed to decode AST: %w", ErrClientError, err)
	}

	p := printer.NewPrinter(printer.PrinterOptions{
		PreserveSourceNewlines:        params.PreserveSourceNewlines,
		NeverAsciiEscape:              params.NeverAsciiEscape,
		TerminateUnterminatedLiterals: params.TerminateUnterminatedLiterals,
	}, printer.PrintHandlers{}, nil)
	return p.Emit(node, nil), nil
}

// handleGetIntrinsicType returns an intrinsic type (any, string, number, etc.).
func (s *Session) handleGetIntrinsicType(ctx context.Context, params *GetIntrinsicTypeParams, getter func(*checker.Checker) *checker.Type) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t := getter(setup.checker)
	if t == nil {
		return nil, nil
	}

	return setup.newTypeResponse(t), nil
}

// handleGetWellKnownSymbols returns the handle ids of the per-checker singleton
// symbols (unknown, undefined, arguments) so the client can identify them by id.
func (s *Session) handleGetWellKnownSymbols(ctx context.Context, params *GetIntrinsicTypeParams) (*WellKnownSymbolsResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	unknown, _ := setup.sd.registerSymbol(setup.checker.GetUnknownSymbol(), setup.projectID)
	undefined, _ := setup.sd.registerSymbol(setup.checker.GetUndefinedSymbol(), setup.projectID)
	arguments, _ := setup.sd.registerSymbol(setup.checker.GetArgumentsSymbol(), setup.projectID)
	return &WellKnownSymbolsResponse{
		Unknown:   unknown,
		Undefined: undefined,
		Arguments: arguments,
	}, nil
}

// handleGetWellKnownSignatures returns the handle id of the per-checker unknown
// signature so the client can identify it by id.
func (s *Session) handleGetWellKnownSignatures(ctx context.Context, params *GetIntrinsicTypeParams) (*WellKnownSignaturesResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	return &WellKnownSignaturesResponse{
		Unknown: setup.sd.registerSignature(setup.projectID, setup.checker.GetUnknownSignature()),
	}, nil
}

// handleIsContextSensitive returns whether a node is context-sensitive.
func (s *Session) handleIsContextSensitive(ctx context.Context, params *GetContextualTypeParams) (bool, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return false, err
	}
	defer setup.done()

	node, err := setup.sd.resolveNodeHandle(setup.program, params.Location)
	if err != nil {
		return false, err
	}
	if node == nil {
		return false, nil
	}

	return setup.checker.IsContextSensitive(node), nil
}

// handleGetReturnTypeOfSignature returns the return type of a signature.
func (s *Session) handleGetReturnTypeOfSignature(ctx context.Context, params *CheckerSignatureParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	sig, err := setup.resolveSignatureHandle(params.Signature)
	if err != nil {
		return nil, err
	}

	return setup.newTypeResponse(setup.checker.GetReturnTypeOfSignature(sig)), nil
}

// handleGetRestTypeOfSignature returns the rest type of a signature.
func (s *Session) handleGetRestTypeOfSignature(ctx context.Context, params *CheckerSignatureParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	sig, err := setup.resolveSignatureHandle(params.Signature)
	if err != nil {
		return nil, err
	}

	return setup.newTypeResponse(setup.checker.GetRestTypeOfSignature(sig)), nil
}

// handleGetTypePredicateOfSignature returns the type predicate of a signature.
func (s *Session) handleGetTypePredicateOfSignature(ctx context.Context, params *CheckerSignatureParams) (*TypePredicateResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	sig, err := setup.resolveSignatureHandle(params.Signature)
	if err != nil {
		return nil, err
	}

	pred := setup.checker.GetTypePredicateOfSignature(sig)
	if pred == nil {
		return nil, nil
	}

	resp := &TypePredicateResponse{
		Kind:           int32(pred.Kind()),
		ParameterIndex: pred.ParameterIndex(),
		ParameterName:  pred.ParameterName(),
	}
	if pred.Type() != nil {
		resp.Type = setup.newTypeResponse(pred.Type())
	}

	return resp, nil
}

// handleIsArrayType returns whether a type is Array<T> or ReadonlyArray<T>.
func (s *Session) handleIsArrayType(ctx context.Context, params *CheckerTypeParams) (bool, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return false, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return false, err
	}

	return setup.checker.IsArrayType(t), nil
}

// handleIsTupleType returns whether a type is a tuple type.
func (s *Session) handleIsTupleType(ctx context.Context, params *CheckerTypeParams) (bool, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return false, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return false, err
	}

	return checker.IsTupleType(t), nil
}

// handleGetBaseTypes returns the base types of an interface/class type.
func (s *Session) handleGetBaseTypes(ctx context.Context, params *CheckerTypeParams) ([]*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return nil, err
	}

	baseTypes := setup.checker.GetBaseTypes(t)
	if len(baseTypes) == 0 {
		return nil, nil
	}

	results := make([]*TypeResponse, len(baseTypes))
	for i, bt := range baseTypes {
		results[i] = setup.newTypeResponse(bt)
	}

	return results, nil
}

// handleGetPropertiesOfType returns the properties of a type.
func (s *Session) handleGetPropertiesOfType(ctx context.Context, params *CheckerTypeParams) ([]*SymbolResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return nil, err
	}

	props := setup.checker.GetPropertiesOfType(t)
	if len(props) == 0 {
		return nil, nil
	}

	results := make([]*SymbolResponse, len(props))
	for i, prop := range props {
		results[i] = setup.newSymbolResponse(prop)
	}

	return results, nil
}

// handleGetApparentType returns the apparent type of a type.
func (s *Session) handleGetApparentType(ctx context.Context, params *CheckerTypeParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return nil, err
	}

	return setup.newTypeResponse(setup.checker.GetApparentType(t)), nil
}

// handleGetIndexInfosOfType returns the index infos of a type.
func (s *Session) handleGetIndexInfosOfType(ctx context.Context, params *CheckerTypeParams) ([]*IndexInfoResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return nil, err
	}

	infos := setup.checker.GetIndexInfosOfType(t)
	if len(infos) == 0 {
		return nil, nil
	}

	results := make([]*IndexInfoResponse, len(infos))
	for i, info := range infos {
		results[i] = &IndexInfoResponse{
			KeyType:    *setup.newTypeResponse(info.KeyType()),
			ValueType:  *setup.newTypeResponse(info.ValueType()),
			IsReadonly: info.IsReadonly(),
		}
		if info.Declaration() != nil {
			results[i].Declaration = setup.sd.nodeHandleFrom(info.Declaration())
		}
	}

	return results, nil
}

// handleGetConstraintOfTypeParameter returns the constraint of a type parameter.
func (s *Session) handleGetConstraintOfTypeParameter(ctx context.Context, params *CheckerTypeParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return nil, err
	}

	constraint := setup.checker.GetConstraintOfTypeParameter(t)
	if constraint == nil {
		return nil, nil
	}

	return setup.newTypeResponse(constraint), nil
}

// handleGetBaseConstraintOfType returns the base constraint of an instantiable type.
func (s *Session) handleGetBaseConstraintOfType(ctx context.Context, params *CheckerTypeParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return nil, err
	}

	constraint := setup.checker.GetBaseConstraintOfType(t)
	if constraint == nil {
		return nil, nil
	}

	return setup.newTypeResponse(constraint), nil
}

// handleGetPropertyOfType returns a named property symbol of a type.
func (s *Session) handleGetPropertyOfType(ctx context.Context, params *GetPropertyOfTypeParams) (*SymbolResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return nil, err
	}

	prop := setup.checker.GetPropertyOfType(t, params.Name)
	if prop == nil {
		return nil, nil
	}

	return setup.newSymbolResponse(prop), nil
}

// handleGetConstantValue returns the constant value of an enum member or const enum access.
func (s *Session) handleGetConstantValue(ctx context.Context, params *CheckerNodeParams) (any, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	node, err := setup.sd.resolveNodeHandle(setup.program, params.Location)
	if err != nil {
		return nil, err
	}
	if node == nil {
		return nil, nil
	}

	return literalValueToJSON(setup.checker.GetConstantValue(node)), nil
}

// handleGetSignatureFromDeclaration returns the signature of a function-like declaration.
func (s *Session) handleGetSignatureFromDeclaration(ctx context.Context, params *CheckerNodeParams) (*SignatureResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	node, err := setup.sd.resolveNodeHandle(setup.program, params.Location)
	if err != nil {
		return nil, err
	}

	return setup.newSignatureResponse(setup.checker.GetSignatureFromDeclaration(node)), nil
}

// handleGetExportSpecifierLocalTargetSymbol returns the local target symbol of an export specifier.
func (s *Session) handleGetExportSpecifierLocalTargetSymbol(ctx context.Context, params *CheckerNodeParams) (*SymbolResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	node, err := setup.sd.resolveNodeHandle(setup.program, params.Location)
	if err != nil {
		return nil, err
	}
	if node == nil {
		return nil, nil
	}

	symbol := setup.checker.GetExportSpecifierLocalTargetSymbol(node)
	if symbol == nil {
		return nil, nil
	}

	return setup.newSymbolResponse(symbol), nil
}

// handleGetAliasedSymbol resolves an alias symbol to its target.
func (s *Session) handleGetAliasedSymbol(ctx context.Context, params *CheckerSymbolParams) (*SymbolResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	symbol, err := setup.resolveSymbolHandle(params.Symbol)
	if err != nil {
		return nil, err
	}

	return setup.newSymbolResponse(setup.checker.GetAliasedSymbol(symbol)), nil
}

// handleGetImmediateAliasedSymbol resolves one level of alias indirection.
func (s *Session) handleGetImmediateAliasedSymbol(ctx context.Context, params *CheckerSymbolParams) (*SymbolResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	symbol, err := setup.resolveSymbolHandle(params.Symbol)
	if err != nil {
		return nil, err
	}
	if symbol == nil {
		return nil, nil
	}

	aliased := setup.checker.GetImmediateAliasedSymbol(symbol)
	if aliased == nil {
		return nil, nil
	}

	return setup.newSymbolResponse(aliased), nil
}

// handleGetExportsOfModule returns the resolved exports of a module symbol,
// including those introduced by `export *` and re-exports.
func (s *Session) handleGetExportsOfModule(ctx context.Context, params *CheckerSymbolParams) ([]*SymbolResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	symbol, err := setup.resolveSymbolHandle(params.Symbol)
	if err != nil {
		return nil, err
	}
	if symbol == nil {
		return nil, nil
	}

	exports := setup.checker.GetExportsOfModule(symbol)
	if len(exports) == 0 {
		return nil, nil
	}
	slices.SortFunc(exports, setup.checker.CompareSymbols)

	results := make([]*SymbolResponse, len(exports))
	for i, exp := range exports {
		results[i] = setup.newSymbolResponse(exp)
	}

	return results, nil
}

// handleGetMemberInModuleExports returns an export by name from a module symbol.
func (s *Session) handleGetMemberInModuleExports(ctx context.Context, params *GetMemberInModuleExportsParams) (*SymbolResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	symbol, err := setup.resolveSymbolHandle(params.Symbol)
	if err != nil {
		return nil, err
	}
	if symbol == nil {
		return nil, nil
	}

	member := setup.checker.TryGetMemberInModuleExports(params.Name, symbol)
	if member == nil {
		return nil, nil
	}

	return setup.newSymbolResponse(member), nil
}

// handleGetJSDocTags returns the JSDoc tags of a symbol as structured name/text pairs.
func (s *Session) handleGetJSDocTags(ctx context.Context, params *CheckerSymbolParams) ([]*JSDocTagInfo, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	symbol, err := setup.resolveSymbolHandle(params.Symbol)
	if err != nil {
		return nil, err
	}
	if symbol == nil {
		return nil, nil
	}

	langSvc, err := s.setupLanguageService(setup.sd, setup.program, params.Project, "")
	if err != nil {
		return nil, err
	}

	tags := langSvc.GetSymbolJSDocTags(symbol)
	if len(tags) == 0 {
		return nil, nil
	}
	results := make([]*JSDocTagInfo, len(tags))
	for i, tag := range tags {
		results[i] = &JSDocTagInfo{Name: tag.Name, Text: tag.Text}
	}
	return results, nil
}

// handleGetDocumentationComment returns the rendered documentation comment of a symbol as plain text.
func (s *Session) handleGetDocumentationComment(ctx context.Context, params *CheckerSymbolParams) (string, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return "", err
	}
	defer setup.done()

	symbol, err := setup.resolveSymbolHandle(params.Symbol)
	if err != nil {
		return "", err
	}
	if symbol == nil {
		return "", nil
	}

	langSvc, err := s.setupLanguageService(setup.sd, setup.program, params.Project, "")
	if err != nil {
		return "", err
	}

	return langSvc.GetSymbolDocumentationComment(setup.checker, symbol), nil
}

// handleGetTypeArguments returns the type arguments of a type reference.
func (s *Session) handleGetTypeArguments(ctx context.Context, params *CheckerTypeParams) ([]*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.resolveTypeHandle(params.Type)
	if err != nil {
		return nil, err
	}

	typeArgs := setup.checker.GetTypeArguments(t)
	if len(typeArgs) == 0 {
		return nil, nil
	}

	results := make([]*TypeResponse, len(typeArgs))
	for i, ta := range typeArgs {
		results[i] = setup.newTypeResponse(ta)
	}

	return results, nil
}

func (s *Session) handleGetTrueTypeOfConditionalType(ctx context.Context, params *GetTypePropertyParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.sd.resolveTypeHandle(params.Project, params.Type)
	if err != nil {
		return nil, err
	}

	return setup.sd.newTypeResponse(params.Project, setup.checker.GetTrueTypeOfConditionalType(t)), nil
}

func (s *Session) handleGetFalseTypeOfConditionalType(ctx context.Context, params *GetTypePropertyParams) (*TypeResponse, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	t, err := setup.sd.resolveTypeHandle(params.Project, params.Type)
	if err != nil {
		return nil, err
	}

	return setup.sd.newTypeResponse(params.Project, setup.checker.GetFalseTypeOfConditionalType(t)), nil
}

func (sd *snapshotData) resolveNodeHandle(program *compiler.Program, handle NodeHandle) (*ast.Node, error) {
	s := string(handle)
	// Format: "index.kind.path" — we need index and path, kind is informational only.
	firstDot := strings.IndexByte(s, '.')
	if firstDot == -1 {
		return nil, fmt.Errorf("%w: invalid node handle %q", ErrClientError, handle)
	}
	secondDot := strings.IndexByte(s[firstDot+1:], '.')
	if secondDot == -1 {
		return nil, fmt.Errorf("%w: invalid node handle %q", ErrClientError, handle)
	}
	secondDot += firstDot + 1 // adjust to absolute index

	idx, err := strconv.ParseUint(s[:firstDot], 10, 32)
	if err != nil {
		return nil, fmt.Errorf("%w: invalid node handle %q: %w", ErrClientError, handle, err)
	}
	path := tspath.Path(s[secondDot+1:])

	sourceFile := program.GetSourceFileByPath(path)
	if sourceFile == nil {
		return nil, fmt.Errorf("%w: node handle %q could not be resolved (file may not be loaded or handle may be stale)", ErrClientError, handle)
	}
	table := encoder.GetNodeIndexTable(sourceFile)

	if table != nil && idx < uint64(len(table.Nodes)) {
		node := table.Nodes[idx]
		if node != nil {
			return node, nil
		}
	}
	return nil, fmt.Errorf("%w: node handle %q could not be resolved (file may not be loaded or handle may be stale)", ErrClientError, handle)
}

// computeSnapshotChanges computes the per-project source file differences between
// two snapshots. It uses DiffOrderedMaps on projects to find changed/removed projects,
// then DiffMaps on FilesByPath for each changed project to collect file-level changes.
func computeSnapshotChanges(prev *project.Snapshot, next *project.Snapshot) *SnapshotChanges {
	prevProjects := prev.ProjectCollection.ProjectsByPath()
	nextProjects := next.ProjectCollection.ProjectsByPath()

	var changes SnapshotChanges

	collections.DiffOrderedMaps(
		prevProjects, nextProjects,
		// onAdded: new project — nothing to retain from previous snapshot.
		func(_ tspath.Path, _ *project.Project) {},
		// onRemoved: project removed entirely.
		func(_ tspath.Path, oldProj *project.Project) {
			changes.RemovedProjects = append(changes.RemovedProjects, ProjectHandle(oldProj))
		},
		// onModified: project changed, diff its files.
		func(_ tspath.Path, oldProj *project.Project, newProj *project.Project) {
			if oldProj.GetProgram() == newProj.GetProgram() {
				return
			}
			var oldFiles, newFiles map[tspath.Path]*ast.SourceFile
			if p := oldProj.GetProgram(); p != nil {
				oldFiles = p.FilesByPath()
			}
			if p := newProj.GetProgram(); p != nil {
				newFiles = p.FilesByPath()
			}
			var projectChanges ProjectFileChanges
			core.DiffMaps(
				oldFiles, newFiles,
				nil, // onAdded: new file in project, not a change.
				func(path tspath.Path, _ *ast.SourceFile) {
					projectChanges.DeletedFiles = append(projectChanges.DeletedFiles, path)
				},
				func(path tspath.Path, _ *ast.SourceFile, _ *ast.SourceFile) {
					projectChanges.ChangedFiles = append(projectChanges.ChangedFiles, path)
				},
			)
			if len(projectChanges.ChangedFiles) > 0 || len(projectChanges.DeletedFiles) > 0 {
				if changes.ChangedProjects == nil {
					changes.ChangedProjects = make(map[ProjectID]*ProjectFileChanges)
				}
				changes.ChangedProjects[ProjectHandle(newProj)] = &projectChanges
			}
		},
	)

	return &changes
}

// Close closes the session and releases all active snapshots,
// regardless of their ref counts.
func (s *Session) Close() {
	s.releaseOpenRefs()

	s.snapshotsMu.Lock()
	defer s.snapshotsMu.Unlock()
	for handle, sd := range s.snapshots {
		sd.snapshot.Deref(s.projectSession)
		delete(s.snapshots, handle)
	}
}

// releaseOpenRefs releases every project and file ref this session is holding open
// in the project session. This keeps the API's ref counts balanced when an API
// session is shut down while sharing a longer-lived project session (e.g. one
// backing an LSP server), so API-opened projects and files aren't leaked. Only
// refs the session currently holds are closed, so it never over-releases.
func (s *Session) releaseOpenRefs() {
	s.updateMu.Lock()
	defer s.updateMu.Unlock()

	if s.openProjects.Len() == 0 && s.openFiles.Len() == 0 {
		return
	}

	apiRequest := &project.APISnapshotRequest{}
	if s.openProjects.Len() > 0 {
		apiRequest.CloseProjects = s.openProjects.Clone()
	}
	if s.openFiles.Len() > 0 {
		apiRequest.CloseFiles = s.openFiles.Clone()
	}
	snapshot, err := s.projectSession.APIUpdate(context.Background(), project.FileChangeSummary{}, apiRequest)
	// APIUpdate returns a ref'd snapshot even on error; always release it.
	snapshot.Deref(s.projectSession)
	if err != nil {
		return
	}

	s.openProjects.Clear()
	s.openFiles.Clear()
}

func formatSessionID(id uint64) string {
	return fmt.Sprintf("api-session-%d", id)
}

// toPath converts a file name to a normalized path.
func (s *Session) toPath(fileName string) tspath.Path {
	return tspath.ToPath(fileName, s.projectSession.GetCurrentDirectory(), s.projectSession.FS().UseCaseSensitiveFileNames())
}

// toFileChangeSummary converts API file changes to a project.FileChangeSummary.
func (s *Session) toFileChangeSummary(changes *APIFileChanges) project.FileChangeSummary {
	if changes == nil {
		return project.FileChangeSummary{}
	}
	var summary project.FileChangeSummary
	if changes.InvalidateAll {
		summary.InvalidateAll = true
		summary.IncludesWatchChangeOutsideNodeModules = true
		return summary
	}
	cwd := s.projectSession.GetCurrentDirectory()
	for _, doc := range changes.Changed {
		uri := doc.ToURI(cwd)
		summary.Changed.Add(uri)
	}
	for _, doc := range changes.Created {
		uri := doc.ToURI(cwd)
		summary.Created.Add(uri)
	}
	for _, doc := range changes.Deleted {
		uri := doc.ToURI(cwd)
		summary.Deleted.Add(uri)
	}
	if summary.Changed.Len()+summary.Created.Len()+summary.Deleted.Len() > 0 {
		summary.IncludesWatchChangeOutsideNodeModules = true
	}
	return summary
}

// handleGetSyntacticDiagnostics returns syntactic diagnostics for a file or all files.
func (s *Session) handleGetSyntacticDiagnostics(ctx context.Context, params *GetDiagnosticsParams) ([]*DiagnosticResponse, error) {
	ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics)
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	program, err := sd.getProgram(params.Project)
	if err != nil {
		return nil, err
	}

	sourceFile, err := s.resolveOptionalSourceFile(program, params.File)
	if err != nil {
		return nil, err
	}

	diags := program.GetSyntacticDiagnostics(ctx, sourceFile)
	return NewDiagnosticResponses(diags), nil
}

// handleGetBindDiagnostics returns bind diagnostics for a file or all files.
func (s *Session) handleGetBindDiagnostics(ctx context.Context, params *GetDiagnosticsParams) ([]*DiagnosticResponse, error) {
	ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics)
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	program, err := sd.getProgram(params.Project)
	if err != nil {
		return nil, err
	}

	sourceFile, err := s.resolveOptionalSourceFile(program, params.File)
	if err != nil {
		return nil, err
	}

	diags := program.GetBindDiagnostics(ctx, sourceFile)
	return NewDiagnosticResponses(diags), nil
}

// handleGetSemanticDiagnostics returns semantic diagnostics for a file or all files.
func (s *Session) handleGetSemanticDiagnostics(ctx context.Context, params *GetDiagnosticsParams) ([]*DiagnosticResponse, error) {
	ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics)
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	program, err := sd.getProgram(params.Project)
	if err != nil {
		return nil, err
	}

	sourceFile, err := s.resolveOptionalSourceFile(program, params.File)
	if err != nil {
		return nil, err
	}

	diags := program.GetSemanticDiagnostics(ctx, sourceFile)
	return NewDiagnosticResponses(diags), nil
}

// handleGetSuggestionDiagnostics returns suggestion diagnostics for a file or all files.
func (s *Session) handleGetSuggestionDiagnostics(ctx context.Context, params *GetDiagnosticsParams) ([]*DiagnosticResponse, error) {
	ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics)
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	program, err := sd.getProgram(params.Project)
	if err != nil {
		return nil, err
	}

	sourceFile, err := s.resolveOptionalSourceFile(program, params.File)
	if err != nil {
		return nil, err
	}

	diags := program.GetSuggestionDiagnostics(ctx, sourceFile)
	return NewDiagnosticResponses(diags), nil
}

// handleGetDeclarationDiagnostics returns declaration diagnostics for a file or all files.
func (s *Session) handleGetDeclarationDiagnostics(ctx context.Context, params *GetDiagnosticsParams) ([]*DiagnosticResponse, error) {
	ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics)
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	program, err := sd.getProgram(params.Project)
	if err != nil {
		return nil, err
	}

	sourceFile, err := s.resolveOptionalSourceFile(program, params.File)
	if err != nil {
		return nil, err
	}

	diags := program.GetDeclarationDiagnostics(ctx, sourceFile)
	return NewDiagnosticResponses(diags), nil
}

// handleGetConfigFileParsingDiagnostics returns config file parsing diagnostics.
func (s *Session) handleGetConfigFileParsingDiagnostics(ctx context.Context, params *GetProjectDiagnosticsParams) ([]*DiagnosticResponse, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	program, err := sd.getProgram(params.Project)
	if err != nil {
		return nil, err
	}

	diags := program.GetConfigFileParsingDiagnostics()
	return NewDiagnosticResponses(diags), nil
}

// handleGetProgramDiagnostics returns program-wide diagnostics, including options diagnostics.
func (s *Session) handleGetProgramDiagnostics(ctx context.Context, params *GetProjectDiagnosticsParams) ([]*DiagnosticResponse, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	program, err := sd.getProgram(params.Project)
	if err != nil {
		return nil, err
	}

	diags := program.GetProgramDiagnostics()
	return NewDiagnosticResponses(diags), nil
}

// handleGetGlobalDiagnostics returns global (non-file-specific) semantic diagnostics.
func (s *Session) handleGetGlobalDiagnostics(ctx context.Context, params *GetProjectDiagnosticsParams) ([]*DiagnosticResponse, error) {
	ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics)
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	proj, err := sd.getProject(params.Project)
	if err != nil {
		return nil, err
	}

	program := proj.GetProgram()
	if program == nil {
		return nil, fmt.Errorf("%w: project has no program", ErrClientError)
	}

	// Global diagnostics are accumulated lazily by the project's checker pool as
	// files are checked. Force a full semantic pass so any global (non-file-specific)
	// diagnostics are produced; otherwise this would return an empty result for
	// projects using an external checker pool (the typical API case), since
	// compiler.Program.GetGlobalDiagnostics only reports for the internal pool.
	program.GetSemanticDiagnostics(ctx, nil)

	diags := core.Filter(proj.GetProjectDiagnostics(ctx), func(d *ast.Diagnostic) bool {
		return d.File() == nil
	})
	return NewDiagnosticResponses(diags), nil
}

// resolveOptionalSourceFile resolves an optional DocumentIdentifier to a source file.
// Returns nil if the identifier is nil (meaning all files).
func (s *Session) resolveOptionalSourceFile(program *compiler.Program, file *DocumentIdentifier) (*ast.SourceFile, error) {
	if file == nil {
		return nil, nil
	}
	sourceFile := program.GetSourceFile(file.ToFileName())
	if sourceFile == nil {
		return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, file)
	}
	return sourceFile, nil
}

// handleGetReferencesToSymbolInFile returns node handles for all identifiers in a file that reference the given symbol.
func (s *Session) handleGetReferencesToSymbolInFile(ctx context.Context, params *GetReferencesToSymbolInFileParams) ([]NodeHandle, error) {
	setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)
	if err != nil {
		return nil, err
	}
	defer setup.done()

	symbol, err := setup.resolveSymbolHandle(params.Symbol)
	if err != nil {
		return nil, err
	}
	if symbol == nil {
		return nil, nil
	}

	sourceFile := setup.program.GetSourceFile(params.File.ToFileName())
	if sourceFile == nil {
		return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File)
	}

	nodes := setup.checker.GetReferencesToSymbolInFile(sourceFile, symbol)
	result := make([]NodeHandle, len(nodes))
	for i, node := range nodes {
		result[i] = setup.sd.nodeHandleFrom(node)
	}
	return result, nil
}

func (s *Session) handleGetSignatureUsages(ctx context.Context, params *GetSignatureUsagesParams) ([]SignatureUsageResponse, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}
	program, err := sd.getProgram(params.Project)
	if err != nil {
		return nil, err
	}

	signatureDecl, err := sd.resolveNodeHandle(program, params.SignatureDecl)
	if err != nil {
		return nil, err
	}
	if signatureDecl == nil {
		return nil, nil
	}

	langSvc, err := s.setupLanguageService(sd, program, params.Project, "")
	if err != nil {
		return nil, err
	}

	usages := langSvc.GetSignatureUsages(ctx, signatureDecl)
	if usages == nil {
		return nil, nil
	}

	result := make([]SignatureUsageResponse, 0, len(usages))
	for _, u := range usages {
		entry := SignatureUsageResponse{
			Name: sd.nodeHandleFrom(u.Name),
		}
		if u.Call != nil {
			entry.Call = sd.nodeHandleFrom(u.Call)
		}
		result = append(result, entry)
	}
	return result, nil
}

// handleGetCompletionsAtPosition returns completions at a position in a document.
func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *GetCompletionsAtPositionParams) (*CompletionInfoResponse, error) {
	if params.IncludeSymbol {
		ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeAPI)
	}
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}
	program, err := sd.getProgram(params.Project)
	if err != nil {
		return nil, err
	}
	sourceFile := program.GetSourceFile(params.File.ToFileName())
	if sourceFile == nil {
		return nil, nil
	}
	langSvc, err := s.setupLanguageService(sd, program, params.Project, "")
	if err != nil {
		return nil, err
	}
	positionMap := sourceFile.GetPositionMap()
	internalPos := positionMap.UTF16ToUTF8(int(params.Position))
	result, err := langSvc.GetCompletionsAtPosition(ctx, sourceFile, internalPos, params.TriggerCharacter, params.IncludeSymbol)
	if err != nil || result == nil {
		return nil, err
	}
	entries := make([]*CompletionEntryResponse, 0, len(result.Items))
	for _, item := range result.Items {
		entry := &CompletionEntryResponse{
			Name:       item.Label,
			SortText:   item.SortText,
			InsertText: item.InsertText,
			FilterText: item.FilterText,
			Detail:     item.Detail,
		}
		if item.Kind != nil {
			entry.Kind = uint32(*item.Kind)
		}
		if item.LabelDetails != nil {
			entry.LabelDetails = &CompletionEntryLabelDetailsResponse{
				Detail:      item.LabelDetails.Detail,
				Description: item.LabelDetails.Description,
			}
		}
		if item.Symbol != nil {
			entry.Symbol = sd.newSymbolResponse(item.Symbol, params.Project)
		}
		entries = append(entries, entry)
	}
	return &CompletionInfoResponse{
		IsIncomplete: result.IsIncomplete,
		Entries:      entries,
	}, nil
}

// handleGetReferencedSymbolsForNode returns node handles for all references found at a node.
func (s *Session) handleGetReferencedSymbolsForNode(ctx context.Context, params *GetReferencedSymbolsForNodeParams) ([]ReferencedSymbolEntry, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}
	program, err := sd.getProgram(params.Project)
	if err != nil {
		return nil, err
	}

	node, err := sd.resolveNodeHandle(program, params.Node)
	if err != nil {
		return nil, err
	}
	if node == nil {
		return nil, nil
	}

	langSvc, err := s.setupLanguageService(sd, program, params.Project, "")
	if err != nil {
		return nil, err
	}

	sourceFiles := program.GetSourceFiles()
	entries := langSvc.GetReferencedSymbolsForNode(ctx, params.Position, node, sourceFiles)
	if entries == nil {
		return nil, nil
	}

	var result []ReferencedSymbolEntry
	for _, entry := range entries {
		defNode := entry.DefinitionNode()
		if defNode == nil {
			continue
		}
		var refs []NodeHandle
		for _, ref := range entry.References() {
			if ref.IsNodeEntry() {
				refs = append(refs, sd.nodeHandleFrom(ref.Node()))
			}
		}
		re := ReferencedSymbolEntry{
			Definition: sd.nodeHandleFrom(defNode),
			References: refs,
		}
		if sym := entry.DefinitionSymbol(); sym != nil {
			re.Symbol = sd.newSymbolResponse(sym, params.Project)
		}
		result = append(result, re)
	}
	return result, nil
}
