package lsp

import (
	"context"
	"errors"
	"fmt"
	"io"
	"iter"
	"math/rand/v2"
	"runtime/debug"
	"slices"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/microsoft/typescript-go/internal/api"
	"github.com/microsoft/typescript-go/internal/collections"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/diagnostics"
	"github.com/microsoft/typescript-go/internal/fswatch"
	"github.com/microsoft/typescript-go/internal/json"
	"github.com/microsoft/typescript-go/internal/jsonrpc"
	"github.com/microsoft/typescript-go/internal/locale"
	"github.com/microsoft/typescript-go/internal/ls"
	"github.com/microsoft/typescript-go/internal/ls/lsconv"
	"github.com/microsoft/typescript-go/internal/ls/lsutil"
	"github.com/microsoft/typescript-go/internal/lsp/lsproto"
	"github.com/microsoft/typescript-go/internal/lsp/lspwatcher"
	"github.com/microsoft/typescript-go/internal/pprof"
	"github.com/microsoft/typescript-go/internal/project"
	"github.com/microsoft/typescript-go/internal/project/ata"
	"github.com/microsoft/typescript-go/internal/tspath"
	"github.com/microsoft/typescript-go/internal/vfs"
	"golang.org/x/sync/errgroup"
)

type ServerOptions struct {
	In  Reader
	Out Writer
	Err io.Writer

	Cwd                string
	FS                 vfs.FS
	DefaultLibraryPath string
	TypingsLocation    string
	ParseCache         *project.ParseCache
	NpmInstall         func(cwd string, args []string) ([]byte, error)
	ProgressDelay      time.Duration // delay before showing progress UI; 0 means no delay
	SetParentProcessID func(parentPID int)
}

func NewServer(opts *ServerOptions) *Server {
	if opts.Cwd == "" {
		panic("Cwd is required")
	}

	s := &Server{
		r:                     opts.In,
		w:                     opts.Out,
		stderr:                opts.Err,
		requestQueue:          newDynamicQueue[*lsproto.RequestMessage](),
		outgoingQueue:         newDynamicQueue[*lsproto.Message](),
		pendingClientRequests: make(map[jsonrpc.ID]pendingClientRequest),
		pendingServerRequests: make(map[jsonrpc.ID]chan *lsproto.ResponseMessage),
		cwd:                   opts.Cwd,
		fs:                    opts.FS,
		defaultLibraryPath:    opts.DefaultLibraryPath,
		typingsLocation:       opts.TypingsLocation,
		parseCache:            opts.ParseCache,
		npmInstall:            opts.NpmInstall,
		startWatchdog:         opts.SetParentProcessID,
		initComplete:          make(chan struct{}),
		progressDelay:         opts.ProgressDelay,
	}
	s.logger = newLogger(s)

	return s
}

var (
	fileRenameFilters = []*lsproto.FileOperationFilter{
		{
			Scheme: new("file"),
			Pattern: &lsproto.FileOperationPattern{
				Glob: "**/*.{ts,tsx,js,jsx,cts,cjs,mts,mjs,json}",
			},
		},
	}
	_ ata.NpmExecutor = (*Server)(nil)
	_ project.Client  = (*Server)(nil)
)

type pendingClientRequest struct {
	req    *lsproto.RequestMessage
	cancel context.CancelFunc
}

type Reader interface {
	Read() (*lsproto.Message, error)
}

type Writer interface {
	Write(msg *lsproto.Message) error
}

type lspReader struct {
	r *lsproto.BaseReader
}

type lspWriter struct {
	w *lsproto.BaseWriter
}

func (r *lspReader) Read() (*lsproto.Message, error) {
	data, err := r.r.Read()
	if err != nil {
		return nil, err
	}

	req := &lsproto.Message{}
	if err := json.Unmarshal(data, req); err != nil {
		if errors.Is(err, lsproto.ErrorCodeInvalidParams) {
			return req, fmt.Errorf("%w: %w", lsproto.ErrorCodeInvalidParams, err)
		}
		return nil, fmt.Errorf("%w: %w", lsproto.ErrorCodeInvalidRequest, err)
	}

	return req, nil
}

func ToReader(r io.Reader) Reader {
	return &lspReader{r: lsproto.NewBaseReader(r)}
}

func (w *lspWriter) Write(msg *lsproto.Message) error {
	data, err := json.Marshal(msg)
	if err != nil {
		return fmt.Errorf("failed to marshal message: %w", err)
	}
	return w.w.Write(data)
}

func ToWriter(w io.Writer) Writer {
	return &lspWriter{w: lsproto.NewBaseWriter(w)}
}

var (
	_ Reader = (*lspReader)(nil)
	_ Writer = (*lspWriter)(nil)
)

type Server struct {
	r             Reader
	w             Writer
	backgroundCtx context.Context

	stderr io.Writer

	logger                  *logger
	initStarted             atomic.Bool
	clientSeq               atomic.Int32
	requestQueue            *dynamicQueue[*lsproto.RequestMessage]
	outgoingQueue           *dynamicQueue[*lsproto.Message]
	pendingClientRequests   map[jsonrpc.ID]pendingClientRequest
	pendingClientRequestsMu sync.Mutex
	pendingServerRequests   map[jsonrpc.ID]chan *lsproto.ResponseMessage
	pendingServerRequestsMu sync.Mutex

	cwd                string
	fs                 vfs.FS
	defaultLibraryPath string
	typingsLocation    string

	initializeParams      *lsproto.InitializeParams
	initializationOptions *lsproto.InitializationOptions
	clientCapabilities    lsproto.ResolvedClientCapabilities
	positionEncoding      lsproto.PositionEncodingKind
	locale                locale.Locale

	watchEnabled     bool
	telemetryEnabled bool
	watcherID        atomic.Uint32
	watchers         collections.SyncSet[project.WatcherID]
	// builtinWatcher is non-nil when the server is running its own
	// in-process file watcher instead of using LSP-based watching. It
	// is enabled when the client lacks DynamicRegistration for
	// workspace/didChangeWatchedFiles and the builtin watcher backend
	// supports efficient recursive watching (Windows or FSEvents).
	builtinWatcher *lspwatcher.Watcher

	lastRequestTimeMs atomic.Int64

	session *project.Session

	// apiSessions holds active API sessions keyed by their ID
	apiSessions   map[string]*api.Session
	apiSessionsMu sync.Mutex

	// Test options for initializing session
	client project.Client

	// initComplete is closed when handleInitialized completes.
	// Used by tests to wait for full initialization.
	initComplete chan struct{}

	// !!! temporary; remove when we have `handleDidChangeConfiguration`/implicit project config support
	compilerOptionsForInferredProjects *core.CompilerOptions
	// parseCache can be passed in so separate tests can share ASTs
	parseCache *project.ParseCache

	npmInstall func(cwd string, args []string) ([]byte, error)

	cpuProfiler pprof.CPUProfiler

	progressDelay   time.Duration
	projectProgress *projectLoadingProgress

	startWatchdog func(parentPID int)
}

func (s *Server) Session() *project.Session { return s.session }

// InitComplete returns a channel that is closed when the server has finished
// processing the initialized notification, including the initial configuration
// exchange with the client.
func (s *Server) InitComplete() <-chan struct{} { return s.initComplete }

// WatchFiles implements project.Client.
func (s *Server) WatchFiles(ctx context.Context, id project.WatcherID, watchers []*lsproto.FileSystemWatcher) error {
	if s.builtinWatcher != nil {
		if err := s.builtinWatcher.WatchFiles(string(id), watchers); err != nil {
			return fmt.Errorf("failed to register file watcher: %w", err)
		}
		s.watchers.Add(id)
		return nil
	}
	_, err := sendClientRequest(ctx, s, lsproto.ClientRegisterCapabilityInfo, &lsproto.RegistrationParams{
		Registrations: []*lsproto.Registration{
			{
				Id: string(id),
				RegisterOptions: &lsproto.RegisterOptions{
					WorkspaceDidChangeWatchedFiles: &lsproto.DidChangeWatchedFilesRegistrationOptions{
						Watchers: watchers,
					},
				},
			},
		},
	})
	if err != nil {
		return fmt.Errorf("failed to register file watcher: %w", err)
	}

	s.watchers.Add(id)
	return nil
}

// UnwatchFiles implements project.Client.
func (s *Server) UnwatchFiles(ctx context.Context, id project.WatcherID) error {
	if s.builtinWatcher != nil {
		if !s.watchers.Has(id) {
			return fmt.Errorf("no file watcher exists with ID %s", id)
		}
		if err := s.builtinWatcher.UnwatchFiles(string(id)); err != nil {
			return fmt.Errorf("failed to unregister file watcher: %w", err)
		}
		s.watchers.Delete(id)
		return nil
	}
	if s.watchers.Has(id) {
		_, err := sendClientRequest(ctx, s, lsproto.ClientUnregisterCapabilityInfo, &lsproto.UnregistrationParams{
			Unregisterations: []*lsproto.Unregistration{
				{
					Id:     string(id),
					Method: string(lsproto.MethodWorkspaceDidChangeWatchedFiles),
				},
			},
		})
		if err != nil {
			return fmt.Errorf("failed to unregister file watcher: %w", err)
		}

		s.watchers.Delete(id)
		return nil
	}

	return fmt.Errorf("no file watcher exists with ID %s", id)
}

// RefreshDiagnostics implements project.Client.
func (s *Server) RefreshDiagnostics(ctx context.Context) error {
	if !s.clientCapabilities.Workspace.Diagnostics.RefreshSupport {
		return nil
	}

	if err := ctx.Err(); err != nil {
		return err
	}

	// Fire-and-forget: the client always returns null, and waiting for the response
	// can cause the server to hang if the client is slow or unresponsive.
	// Any response from the client will be silently ignored by the read loop.
	if err := sendClientRequestFireAndForget(s, lsproto.WorkspaceDiagnosticRefreshInfo, lsproto.NoParams{}); err != nil {
		return fmt.Errorf("failed to refresh diagnostics: %w", err)
	}

	return nil
}

// PublishDiagnostics implements project.Client.
func (s *Server) PublishDiagnostics(ctx context.Context, params *lsproto.PublishDiagnosticsParams) error {
	return sendNotification(s, lsproto.TextDocumentPublishDiagnosticsInfo, params)
}

// SendTelemetry implements project.Client.
func (s *Server) SendTelemetry(ctx context.Context, telemetry lsproto.TelemetryEvent) error {
	if !s.telemetryEnabled {
		panic("SendTelemetry called with telemetry disabled")
	}
	return sendNotification(s, lsproto.TelemetryEventInfo, telemetry)
}

// IsActive implements project.Client.
func (s *Server) IsActive() bool {
	last := s.lastRequestTimeMs.Load()
	return last == 0 || time.Since(time.UnixMilli(last)) <= time.Minute
}

func (s *Server) RefreshInlayHints(ctx context.Context) error {
	if !s.clientCapabilities.Workspace.InlayHint.RefreshSupport {
		return nil
	}

	if err := sendClientRequestFireAndForget(s, lsproto.WorkspaceInlayHintRefreshInfo, lsproto.NoParams{}); err != nil {
		return fmt.Errorf("failed to refresh inlay hints: %w", err)
	}
	return nil
}

func (s *Server) RefreshCodeLens(ctx context.Context) error {
	if !s.clientCapabilities.Workspace.CodeLens.RefreshSupport {
		return nil
	}

	if err := sendClientRequestFireAndForget(s, lsproto.WorkspaceCodeLensRefreshInfo, lsproto.NoParams{}); err != nil {
		return fmt.Errorf("failed to refresh code lens: %w", err)
	}
	return nil
}

// ProgressStart implements project.Client.
func (s *Server) ProgressStart(message *diagnostics.Message, args ...any) {
	if s.projectProgress != nil {
		s.projectProgress.start(message, args...)
	}
}

// ProgressFinish implements project.Client.
func (s *Server) ProgressFinish(message *diagnostics.Message, args ...any) {
	if s.projectProgress != nil {
		s.projectProgress.finish(message, args...)
	}
}

func (s *Server) RequestConfiguration(ctx context.Context) (lsutil.UserPreferences, error) {
	caps := lsproto.GetClientCapabilities(ctx)
	if !caps.Workspace.Configuration {
		if opts := s.initializationOptions; opts.UserPreferences != nil {
			userPrefs := *opts.UserPreferences
			s.logger.Logf(
				"received formatting options from initialization: %T\n%+v",
				userPrefs,
				userPrefs,
			)
			if config, ok := userPrefs.(map[string]any); ok {
				return lsutil.ParseUserPreferences(map[string]any{"js/ts": config}), nil
			}
		}
		return lsutil.NewDefaultUserPreferences(), nil
	}
	configs, err := sendClientRequest(ctx, s, lsproto.WorkspaceConfigurationInfo, &lsproto.ConfigurationParams{
		Items: []*lsproto.ConfigurationItem{
			{
				Section: new("js/ts"),
			},
			{
				Section: new("typescript"),
			},
			{
				Section: new("javascript"),
			},
			{
				Section: new("editor"),
			},
		},
	})
	if err != nil {
		return lsutil.UserPreferences{}, fmt.Errorf("configure request failed: %w", err)
	}
	configMap := map[string]any{}
	for i, config := range configs {
		switch i {
		case 0:
			configMap["js/ts"] = config
		case 1:
			configMap["typescript"] = config
		case 2:
			configMap["javascript"] = config
		case 3:
			configMap["editor"] = config
		}
	}
	s.logger.Logf(
		"received options from workspace/configuration request:\njs/ts: %+v\n\ntypescript: %+v\n\njavascript: %+v\n\neditor: %+v\n",
		configMap["js/ts"],
		configMap["typescript"],
		configMap["javascript"],
		configMap["editor"],
	)
	return lsutil.ParseUserPreferences(configMap), nil
}

func (s *Server) Run(ctx context.Context) error {
	g, ctx := errgroup.WithContext(ctx)
	s.backgroundCtx = ctx
	g.Go(func() error { return s.dispatchLoop(ctx) })
	g.Go(func() error { return s.writeLoop(ctx) })

	// Don't run readLoop in the group, as it blocks on stdin read and cannot be cancelled.
	readLoopErr := make(chan error, 1)
	g.Go(func() error {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case err := <-readLoopErr:
			return err
		}
	})
	go func() { readLoopErr <- s.readLoop(ctx) }()

	if err := g.Wait(); err != nil && !errors.Is(err, io.EOF) && ctx.Err() != nil {
		return err
	}
	return nil
}

func (s *Server) readLoop(ctx context.Context) error {
	for {
		if err := ctx.Err(); err != nil {
			return err
		}
		msg, err := s.read()
		if err != nil {
			if errors.Is(err, lsproto.ErrorCodeInvalidRequest) || errors.Is(err, lsproto.ErrorCodeInvalidParams) {
				var id *jsonrpc.ID
				if errors.Is(err, lsproto.ErrorCodeInvalidParams) {
					if msg != nil && msg.Kind == jsonrpc.MessageKindRequest {
						id = msg.AsRequest().ID
					}
				}
				if err := s.sendError(id, err); err != nil {
					return err
				}
				continue
			}
			return err
		}

		if s.initializeParams == nil && msg.Kind == jsonrpc.MessageKindRequest {
			req := msg.AsRequest()
			if req.Method == lsproto.MethodInitialize {
				params, err := lsproto.UnmarshalParams[*lsproto.InitializeParams](req)
				if err != nil {
					if err := s.sendError(req.ID, err); err != nil {
						return err
					}
					continue
				}
				resp, err := s.handleInitialize(ctx, params, req)
				if err != nil {
					return err
				}
				if err := s.sendResult(req.ID, resp); err != nil {
					return err
				}
			} else {
				if err := s.sendError(req.ID, lsproto.ErrorCodeServerNotInitialized); err != nil {
					return err
				}
			}
			continue
		}

		if msg.Kind == jsonrpc.MessageKindResponse {
			resp := msg.AsResponse()
			s.pendingServerRequestsMu.Lock()
			if respChan, ok := s.pendingServerRequests[*resp.ID]; ok {
				respChan <- resp
				close(respChan)
				delete(s.pendingServerRequests, *resp.ID)
			}
			s.pendingServerRequestsMu.Unlock()
		} else {
			req := msg.AsRequest()
			if req.Method == lsproto.MethodCancelRequest {
				if params, err := lsproto.UnmarshalParams[*lsproto.CancelParams](req); err == nil && params != nil {
					s.cancelRequest(params.Id)
				}
			} else {
				if err := s.requestQueue.Put(ctx, req); err != nil {
					return err
				}
			}
		}
	}
}

func (s *Server) cancelRequest(rawID lsproto.IntegerOrString) {
	id := lsproto.NewID(rawID)
	s.pendingClientRequestsMu.Lock()
	defer s.pendingClientRequestsMu.Unlock()
	if pendingReq, ok := s.pendingClientRequests[*id]; ok {
		pendingReq.cancel()
		delete(s.pendingClientRequests, *id)
	}
}

func (s *Server) read() (*lsproto.Message, error) {
	return s.r.Read()
}

func (s *Server) dispatchLoop(ctx context.Context) error {
	ctx, lspExit := context.WithCancelCause(ctx)
	defer lspExit(nil)
	for {
		req, err := s.requestQueue.Get(ctx)
		if err != nil {
			return err
		}

		s.lastRequestTimeMs.Store(time.Now().UnixMilli())
		requestCtx := locale.WithLocale(ctx, s.locale)
		var cancel context.CancelFunc
		if req.ID != nil {
			requestCtx, cancel = context.WithCancel(core.WithRequestID(requestCtx, req.ID.String()))
			s.pendingClientRequestsMu.Lock()
			s.pendingClientRequests[*req.ID] = pendingClientRequest{
				req:    req,
				cancel: cancel,
			}
			s.pendingClientRequestsMu.Unlock()
		}

		handleError := func(err error) {
			if errors.Is(err, context.Canceled) {
				if err := s.sendError(req.ID, lsproto.ErrorCodeRequestCancelled); err != nil {
					lspExit(err)
				}
			} else if errors.Is(err, io.EOF) {
				lspExit(nil)
			} else {
				if err := s.sendError(req.ID, err); err != nil {
					lspExit(err)
				}
			}
		}

		removeRequest := func() {
			if req.ID != nil {
				defer cancel()
				s.pendingClientRequestsMu.Lock()
				defer s.pendingClientRequestsMu.Unlock()
				delete(s.pendingClientRequests, *req.ID)
			}
		}

		if doAsyncWork, err := s.handleRequestOrNotification(requestCtx, req); err != nil {
			handleError(err)
			removeRequest()
		} else if doAsyncWork != nil {
			go func() {
				if lsError := doAsyncWork(); lsError != nil {
					handleError(lsError)
				}
				removeRequest()
			}()
		} else {
			removeRequest()
		}
	}
}

func (s *Server) writeLoop(ctx context.Context) error {
	for {
		msg, err := s.outgoingQueue.Get(ctx)
		if err != nil {
			return err
		}
		if err := s.w.Write(msg); err != nil {
			return fmt.Errorf("failed to write message: %w", err)
		}
	}
}

// WARNING: this should only be called in the async portion of a request handler,
// otherwise a deadlock can occur.
func sendClientRequest[Req, Resp any](ctx context.Context, s *Server, info lsproto.RequestInfo[Req, Resp], params Req) (Resp, error) {
	id := jsonrpc.NewIDString(fmt.Sprintf("ts%d", s.clientSeq.Add(1)))
	req := info.NewRequestMessage(id, params)

	responseChan := make(chan *lsproto.ResponseMessage, 1)
	s.pendingServerRequestsMu.Lock()
	s.pendingServerRequests[*id] = responseChan
	s.pendingServerRequestsMu.Unlock()

	defer func() {
		s.pendingServerRequestsMu.Lock()
		defer s.pendingServerRequestsMu.Unlock()
		if respChan, ok := s.pendingServerRequests[*id]; ok {
			close(respChan)
			delete(s.pendingServerRequests, *id)
		}
	}()

	if err := s.send(req.Message()); err != nil {
		return *new(Resp), err
	}

	select {
	case <-ctx.Done():
		return *new(Resp), ctx.Err()
	case resp := <-responseChan:
		if resp.Error != nil {
			return *new(Resp), fmt.Errorf("request failed: %s", resp.Error.String())
		}
		return info.UnmarshalResult(resp.Result)
	}
}

// sendClientRequestFireAndForget sends a request to the client without waiting for a response.
// The response, if any, will be silently ignored by the read loop since no pending channel is registered.
// This means any error returned by the client will not be observed. Use only for requests where the
// response value is not needed (e.g., the client always returns null).
func sendClientRequestFireAndForget[Req, Resp any](s *Server, info lsproto.RequestInfo[Req, Resp], params Req) error {
	id := jsonrpc.NewIDString(fmt.Sprintf("ts%d", s.clientSeq.Add(1)))
	req := info.NewRequestMessage(id, params)
	return s.send(req.Message())
}

func (s *Server) sendResult(id *jsonrpc.ID, result any) error {
	return s.sendResponse(&lsproto.ResponseMessage{
		ID:     id,
		Result: result,
	})
}

type userFacingRequestFailedError string

func (e userFacingRequestFailedError) Error() string { return string(e) }
func (e userFacingRequestFailedError) Unwrap() error { return lsproto.ErrorCodeRequestFailed }

func (s *Server) sendError(id *jsonrpc.ID, err error) error {
	// Do not send error response for notifications,
	// except for parse errors which may occur before determining if the message is a request or notification.
	if id == nil && !errors.Is(err, lsproto.ErrorCodeInvalidRequest) {
		s.logger.Errorf("error handling notification: %s", err)
		return nil
	}
	code := lsproto.ErrorCodeInternalError
	if errCode, ok := errors.AsType[lsproto.ErrorCode](err); ok {
		code = errCode
	}
	// TODO(jakebailey): error data
	return s.sendResponse(&lsproto.ResponseMessage{
		ID: id,
		Error: &jsonrpc.ResponseError{
			Code:    int32(code),
			Message: err.Error(),
		},
	})
}

func sendNotification[Params any](s *Server, info lsproto.NotificationInfo[Params], params Params) error {
	return s.send(info.NewNotificationMessage(params).Message())
}

func (s *Server) sendResponse(resp *lsproto.ResponseMessage) error {
	return s.send(resp.Message())
}

// send writes a message to the outgoing queue, respecting context cancellation.
func (s *Server) send(msg *lsproto.Message) error {
	return s.outgoingQueue.Put(s.backgroundCtx, msg)
}

// handleRequestOrNotification looks up the handler for the given request or notification, executes its synchronous work
// and returns any asynchronous work as a function to be executed by the caller.
func (s *Server) handleRequestOrNotification(ctx context.Context, req *lsproto.RequestMessage) (func() error, error) {
	ctx = lsproto.WithClientCapabilities(ctx, &s.clientCapabilities)

	if handler := handlers()[req.Method]; handler != nil {
		start := time.Now()
		doAsyncWork, err := handler(s, ctx, req)
		idStr := ""
		if req.ID != nil {
			idStr = " (" + req.ID.String() + ")"
		}
		if err != nil {
			if _, ok := errors.AsType[userFacingRequestFailedError](err); !ok {
				s.logger.Error("error handling method '", req.Method, "'", idStr, ": ", err)
			} else if !s.logger.IsTracing() {
				s.logger.Info("handled method '", req.Method, "'", idStr, " in ", time.Since(start))
			}
			return nil, err
		}
		if doAsyncWork != nil {
			return func() error {
				// note: ctx.Err() has to be checked in the async work to allow async handlers to cleanup resources correctly
				asyncWorkErr := doAsyncWork()
				_, isUserFacing := errors.AsType[userFacingRequestFailedError](asyncWorkErr)
				isRealError := asyncWorkErr != nil && !isUserFacing
				if isRealError {
					s.logger.Info("error handling method '", req.Method, "'", idStr, " in ", time.Since(start))
				} else if !s.logger.IsTracing() {
					s.logger.Info("handled method '", req.Method, "'", idStr, " in ", time.Since(start))
				}
				return asyncWorkErr
			}, nil
		}
		if !s.logger.IsTracing() {
			s.logger.Info("handled method '", req.Method, "'", idStr, " in ", time.Since(start))
		}
		return nil, nil
	}
	s.logger.Warn("unknown method '", req.Method, "'")
	if req.ID != nil {
		return nil, s.sendError(req.ID, lsproto.ErrorCodeInvalidRequest)
	}
	return nil, nil
}

// handlerMap maps LSP method to a handler function. The handler function executes any work that must be done synchronously
// before other requests/notifications can be processed, and returns any additional work as a function to be executed
// asynchronously after the synchronous work is complete.
type handlerMap map[lsproto.Method]func(*Server, context.Context, *lsproto.RequestMessage) (func() error, error)

var handlers = sync.OnceValue(func() handlerMap {
	handlers := make(handlerMap)

	registerRequestHandler(handlers, lsproto.InitializeInfo, (*Server).handleInitialize)
	registerNotificationHandler(handlers, lsproto.InitializedInfo, (*Server).handleInitialized)
	registerRequestHandler(handlers, lsproto.ShutdownInfo, (*Server).handleShutdown)
	registerNotificationHandler(handlers, lsproto.ExitInfo, (*Server).handleExit)

	registerNotificationHandler(handlers, lsproto.WorkspaceDidChangeConfigurationInfo, (*Server).handleDidChangeWorkspaceConfiguration)
	registerNotificationHandler(handlers, lsproto.TextDocumentDidOpenInfo, (*Server).handleDidOpen)
	registerNotificationHandler(handlers, lsproto.TextDocumentDidChangeInfo, (*Server).handleDidChange)
	registerNotificationHandler(handlers, lsproto.TextDocumentDidSaveInfo, (*Server).handleDidSave)
	registerNotificationHandler(handlers, lsproto.TextDocumentDidCloseInfo, (*Server).handleDidClose)
	registerNotificationHandler(handlers, lsproto.WorkspaceDidChangeWatchedFilesInfo, (*Server).handleDidChangeWatchedFiles)
	registerNotificationHandler(handlers, lsproto.SetTraceInfo, (*Server).handleSetTrace)
	registerNotificationHandler(handlers, lsproto.CustomSetLogVerbosityInfo, (*Server).handleSetLogVerbosity)
	registerRequestHandler(handlers, lsproto.WorkspaceWillRenameFilesInfo, (*Server).handleWillRenameFiles)

	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDiagnosticInfo, (*Server).handleDocumentDiagnostic)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentHoverInfo, (*Server).handleHover)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDefinitionInfo, (*Server).handleDefinition)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.CustomTextDocumentSourceDefinitionInfo, (*Server).handleSourceDefinition)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentTypeDefinitionInfo, (*Server).handleTypeDefinition)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentSignatureHelpInfo, (*Server).handleSignatureHelp)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentFormattingInfo, (*Server).handleDocumentFormat)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentRangeFormattingInfo, (*Server).handleDocumentRangeFormat)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentOnTypeFormattingInfo, (*Server).handleDocumentOnTypeFormat)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDocumentSymbolInfo, (*Server).handleDocumentSymbol)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDocumentHighlightInfo, (*Server).handleDocumentHighlight)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.CustomTextDocumentMultiDocumentHighlightInfo, (*Server).handleMultiDocumentHighlight)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentSelectionRangeInfo, (*Server).handleSelectionRange)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentInlayHintInfo, (*Server).handleInlayHint)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentCodeLensInfo, (*Server).handleCodeLens)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentCodeActionInfo, (*Server).handleCodeAction)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentPrepareCallHierarchyInfo, (*Server).handlePrepareCallHierarchy)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentFoldingRangeInfo, (*Server).handleFoldingRange)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentPrepareRenameInfo, (*Server).handlePrepareRename)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentLinkedEditingRangeInfo, (*Server).handleLinkedEditingRange)

	registerLanguageServiceWithAutoImportsRequestHandler(handlers, lsproto.TextDocumentCompletionInfo, (*Server).handleCompletion)
	registerLanguageServiceWithAutoImportsRequestHandler(handlers, lsproto.TextDocumentCodeActionInfo, (*Server).handleCodeAction)

	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentVSOnAutoInsertInfo, (*Server).handleVSOnAutoInsert)

	registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentReferencesInfo, (*ls.LanguageService).ProvideReferences)
	registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentVSReferencesInfo, (*ls.LanguageService).ProvideVSReferences)
	registerRequestHandler(handlers, lsproto.TextDocumentRenameInfo, (*Server).handleRename)
	registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentImplementationInfo, (*ls.LanguageService).ProvideImplementations)

	registerRequestHandler(handlers, lsproto.CallHierarchyIncomingCallsInfo, (*Server).handleCallHierarchyIncomingCalls)
	registerRequestHandler(handlers, lsproto.CallHierarchyOutgoingCallsInfo, (*Server).handleCallHierarchyOutgoingCalls)

	registerRequestHandler(handlers, lsproto.WorkspaceSymbolInfo, (*Server).handleWorkspaceSymbol)
	registerRequestHandler(handlers, lsproto.CompletionItemResolveInfo, (*Server).handleCompletionItemResolve)
	registerRequestHandler(handlers, lsproto.CodeLensResolveInfo, (*Server).handleCodeLensResolve)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentSemanticTokensFullInfo, (*Server).handleSemanticTokensFull)
	registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentSemanticTokensRangeInfo, (*Server).handleSemanticTokensRange)

	// Developer/debugging commands
	registerRequestHandler(handlers, lsproto.CustomRunGCInfo, (*Server).handleRunGC)
	registerRequestHandler(handlers, lsproto.CustomSaveHeapProfileInfo, (*Server).handleSaveHeapProfile)
	registerRequestHandler(handlers, lsproto.CustomSaveAllocProfileInfo, (*Server).handleSaveAllocProfile)
	registerRequestHandler(handlers, lsproto.CustomStartCPUProfileInfo, (*Server).handleStartCPUProfile)
	registerRequestHandler(handlers, lsproto.CustomStopCPUProfileInfo, (*Server).handleStopCPUProfile)

	registerRequestHandler(handlers, lsproto.CustomInitializeAPISessionInfo, (*Server).handleInitializeAPISession)
	registerRequestHandler(handlers, lsproto.CustomProjectInfoInfo, (*Server).handleProjectInfo)
	return handlers
})

func registerNotificationHandler[Req any](handlers handlerMap, info lsproto.NotificationInfo[Req], fn func(*Server, context.Context, Req) error) {
	handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) (func() error, error) {
		if s.session == nil && req.Method != lsproto.MethodInitialized {
			return nil, lsproto.ErrorCodeServerNotInitialized
		}

		params, err := lsproto.UnmarshalParams[Req](req)
		if err != nil {
			return nil, err
		}
		if err := fn(s, ctx, params); err != nil {
			return nil, err
		}
		return nil, ctx.Err()
	}
}

func registerRequestHandler[Req, Resp any](
	handlers handlerMap,
	info lsproto.RequestInfo[Req, Resp],
	fn func(*Server, context.Context, Req, *lsproto.RequestMessage) (Resp, error),
) {
	handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) (func() error, error) {
		if s.session == nil && req.Method != lsproto.MethodInitialize {
			return nil, lsproto.ErrorCodeServerNotInitialized
		}

		params, err := lsproto.UnmarshalParams[Req](req)
		if err != nil {
			return nil, err
		}
		resp, err := fn(s, ctx, params, req)
		if err != nil {
			return nil, err
		}
		if ctx.Err() != nil {
			return nil, ctx.Err()
		}
		return nil, s.sendResult(req.ID, resp)
	}
}

func registerLanguageServiceDocumentRequestHandler[Req lsproto.HasTextDocumentURI, Resp any](handlers handlerMap, info lsproto.RequestInfo[Req, Resp], fn func(*Server, context.Context, *ls.LanguageService, Req) (Resp, error)) {
	handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) (func() error, error) {
		params, err := lsproto.UnmarshalParams[Req](req)
		if err != nil {
			return nil, err
		}
		ls, err := s.session.GetLanguageService(ctx, params.TextDocumentURI())
		if err != nil {
			return nil, err
		}
		return func() error {
			defer s.recover(req)
			resp, lsErr := fn(s, ctx, ls, params)
			// After any language service request, check if new global diagnostics were
			// discovered during checking and push updated tsconfig diagnostics if so.
			s.session.EnqueuePublishGlobalDiagnostics()
			if lsErr != nil {
				return lsErr
			}
			if ctx.Err() != nil {
				return ctx.Err()
			}
			return s.sendResult(req.ID, resp)
		}, nil
	}
}

func registerLanguageServiceWithAutoImportsRequestHandler[Req lsproto.HasTextDocumentURI, Resp any](handlers handlerMap, info lsproto.RequestInfo[Req, Resp], fn func(*Server, context.Context, *ls.LanguageService, Req) (Resp, error)) {
	handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) (func() error, error) {
		params, err := lsproto.UnmarshalParams[Req](req)
		if err != nil {
			return nil, err
		}
		return s.session.WithLanguageServiceAndSnapshot(ctx, params.TextDocumentURI(), func(languageService *ls.LanguageService, snapshot *project.Snapshot) (func() error, error) {
			return func() error {
				defer s.recover(req)
				resp, lsErr := fn(s, ctx, languageService, params)
				if errors.Is(lsErr, ls.ErrNeedsAutoImports) {
					languageService, lsErr = s.session.GetLanguageServiceWithAutoImports(ctx, snapshot, params.TextDocumentURI())
					if lsErr != nil {
						return lsErr
					}
					if ctx.Err() != nil {
						return ctx.Err()
					}
					resp, lsErr = fn(s, ctx, languageService, params)
					if errors.Is(lsErr, ls.ErrNeedsAutoImports) {
						panic(info.Method + " returned ErrNeedsAutoImports even after enabling auto imports")
					}
				}
				if lsErr != nil {
					return lsErr
				}
				if ctx.Err() != nil {
					return ctx.Err()
				}
				return s.sendResult(req.ID, resp)
			}, nil
		})
	}
}

func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosition, Resp any](
	handlers handlerMap,
	info lsproto.RequestInfo[Req, Resp],
	fn func(*ls.LanguageService, context.Context, Req, ls.CrossProjectOrchestrator) (Resp, error),
) {
	handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) (func() error, error) {
		params, err := lsproto.UnmarshalParams[Req](req)
		if err != nil {
			return nil, err
		}
		// !!! sheetal: multiple projects that contain the file through symlinks
		defaultLs, orchestrator, err := s.getLanguageServiceAndCrossProjectOrchestrator(ctx, params.TextDocumentURI(), req)
		if err != nil {
			return nil, err
		}
		return func() error {
			defer s.recover(req)
			resp, lsErr := fn(defaultLs, ctx, params, orchestrator)
			if lsErr != nil {
				return lsErr
			}
			if ctx.Err() != nil {
				return ctx.Err()
			}
			return s.sendResult(req.ID, resp)
		}, nil
	}
}

type crossProjectOrchestrator struct {
	server         *Server
	req            *lsproto.RequestMessage
	defaultProject *project.Project
	allProjects    []ls.Project
}

var _ ls.CrossProjectOrchestrator = (*crossProjectOrchestrator)(nil)

func (c *crossProjectOrchestrator) GetDefaultProject() ls.Project {
	return c.defaultProject
}

func (c *crossProjectOrchestrator) GetAllProjectsForInitialRequest() []ls.Project {
	return c.allProjects
}

func (c *crossProjectOrchestrator) GetLanguageServiceForProjectWithFile(ctx context.Context, p ls.Project, uri lsproto.DocumentUri) *ls.LanguageService {
	return c.server.session.GetLanguageServiceForProjectWithFile(ctx, p.(*project.Project), uri)
}

func (c *crossProjectOrchestrator) GetProjectsForFile(ctx context.Context, uri lsproto.DocumentUri) ([]ls.Project, error) {
	return c.server.session.GetProjectsForFile(ctx, uri)
}

func (c *crossProjectOrchestrator) GetProjectsLoadingProjectTree(ctx context.Context, requestedProjectTrees *collections.Set[tspath.Path]) iter.Seq[ls.Project] {
	return func(yield func(ls.Project) bool) {
		c.server.session.WithSnapshotLoadingProjectTree(ctx, requestedProjectTrees, func(snapshot *project.Snapshot) {
			for _, p := range snapshot.ProjectCollection.Projects() {
				if !yield(p) {
					return
				}
			}
		})
	}
}

func (s *Server) getLanguageServiceAndCrossProjectOrchestrator(ctx context.Context, uri lsproto.DocumentUri, req *lsproto.RequestMessage) (*ls.LanguageService, ls.CrossProjectOrchestrator, error) {
	defaultProject, defaultLs, allProjects, err := s.session.GetLanguageServiceAndProjectsForFile(ctx, uri)
	var orchestrator ls.CrossProjectOrchestrator
	if err == nil {
		orchestrator = &crossProjectOrchestrator{s, req, defaultProject, allProjects}
	}
	return defaultLs, orchestrator, err
}

func (s *Server) recover(req *lsproto.RequestMessage) {
	if r := recover(); r != nil {
		stack := debug.Stack()
		s.logger.Errorf("panic handling request %s: %v\n%s", req.Method, r, string(stack))
		if req.ID != nil {
			_ = s.sendError(req.ID, fmt.Errorf("%w: panic handling request %s: %v", lsproto.ErrorCodeInternalError, req.Method, r))
		} else {
			s.logger.Error("unhandled panic in notification", req.Method, r)
		}

		if s.telemetryEnabled {
			_ = sendNotification(s, lsproto.TelemetryEventInfo, lsproto.TelemetryEvent{
				RequestFailureTelemetryEvent: &lsproto.RequestFailureTelemetryEvent{
					Properties: &lsproto.RequestFailureTelemetryProperties{
						ErrorCode:     lsproto.ErrorCodeInternalError.String(),
						RequestMethod: strings.ReplaceAll(string(req.Method), "/", "."),
						Stack:         sanitizeStackTrace(string(stack)),
					},
				},
			})
		}
	}
}

func (s *Server) handleInitialize(ctx context.Context, params *lsproto.InitializeParams, _ *lsproto.RequestMessage) (lsproto.InitializeResponse, error) {
	if s.initializeParams != nil {
		return nil, lsproto.ErrorCodeInvalidRequest
	}

	s.initStarted.Store(true)

	s.initializeParams = params
	// The spec types initializationOptions as nullable; treat both null and an
	// absent value as empty options so the rest of the server can read fields
	// off s.initializationOptions without nil-checking the container.
	if params.InitializationOptions != nil && params.InitializationOptions.InitializationOptions != nil {
		s.initializationOptions = params.InitializationOptions.InitializationOptions
	} else {
		s.initializationOptions = &lsproto.InitializationOptions{}
	}
	if s.initializationOptions.LogVerbosity != nil {
		if v := *s.initializationOptions.LogVerbosity; isValidLogVerbosity(v) {
			s.logger.SetVerbosity(v)
		}
	}
	s.clientCapabilities = params.Capabilities.Resolve()
	if s.clientCapabilities.Window.WorkDoneProgress {
		s.projectProgress = newProjectLoadingProgress(s, s.progressDelay)
	}

	capabilitiesJSON, err := json.MarshalIndent(&s.clientCapabilities, "", "\t")
	if err != nil {
		return nil, err
	}
	s.logger.Info("Resolved client capabilities: " + string(capabilitiesJSON))

	s.positionEncoding = lsproto.PositionEncodingKindUTF16
	if slices.Contains(s.clientCapabilities.General.PositionEncodings, lsproto.PositionEncodingKindUTF8) {
		s.positionEncoding = lsproto.PositionEncodingKindUTF8
	}

	if s.initializeParams.Locale != nil {
		s.locale, _ = locale.Parse(*s.initializeParams.Locale)
	}

	if s.startWatchdog != nil && params.ProcessId.Integer != nil {
		s.startWatchdog(int(*params.ProcessId.Integer))
	}

	response := &lsproto.InitializeResult{
		ServerInfo: &lsproto.ServerInfo{
			Name:    "typescript-go",
			Version: new(core.Version()),
		},
		Capabilities: &lsproto.ServerCapabilities{
			PositionEncoding: new(s.positionEncoding),
			TextDocumentSync: &lsproto.TextDocumentSyncOptionsOrKind{
				Options: &lsproto.TextDocumentSyncOptions{
					OpenClose: new(true),
					Change:    new(lsproto.TextDocumentSyncKindIncremental),
					Save: &lsproto.BooleanOrSaveOptions{
						Boolean: new(true),
					},
				},
			},
			HoverProvider: &lsproto.BooleanOrHoverOptions{
				Boolean: new(true),
			},
			DefinitionProvider: &lsproto.BooleanOrDefinitionOptions{
				Boolean: new(true),
			},
			TypeDefinitionProvider: &lsproto.BooleanOrTypeDefinitionOptionsOrTypeDefinitionRegistrationOptions{
				Boolean: new(true),
			},
			ReferencesProvider: &lsproto.BooleanOrReferenceOptions{
				Boolean: new(true),
			},
			ImplementationProvider: &lsproto.BooleanOrImplementationOptionsOrImplementationRegistrationOptions{
				Boolean: new(true),
			},
			DiagnosticProvider: &lsproto.DiagnosticOptionsOrRegistrationOptions{
				Options: &lsproto.DiagnosticOptions{
					Identifier:            new("typescript"),
					InterFileDependencies: true,
				},
			},
			CompletionProvider: &lsproto.CompletionOptions{
				TriggerCharacters: &ls.TriggerCharacters,
				ResolveProvider:   new(true),
				CompletionItem: &lsproto.ServerCompletionItemOptions{
					LabelDetailsSupport: new(true),
				},
			},
			SignatureHelpProvider: &lsproto.SignatureHelpOptions{
				TriggerCharacters:   &[]string{"(", ",", "<"},
				RetriggerCharacters: &[]string{")"},
			},
			DocumentFormattingProvider: &lsproto.BooleanOrDocumentFormattingOptions{
				Boolean: new(true),
			},
			DocumentRangeFormattingProvider: &lsproto.BooleanOrDocumentRangeFormattingOptions{
				Boolean: new(true),
			},
			DocumentOnTypeFormattingProvider: &lsproto.DocumentOnTypeFormattingOptions{
				FirstTriggerCharacter: "{",
				MoreTriggerCharacter:  &[]string{"}", ";", "\n"},
			},
			WorkspaceSymbolProvider: &lsproto.BooleanOrWorkspaceSymbolOptions{
				Boolean: new(true),
			},
			DocumentSymbolProvider: &lsproto.BooleanOrDocumentSymbolOptions{
				Boolean: new(true),
			},
			FoldingRangeProvider: &lsproto.BooleanOrFoldingRangeOptionsOrFoldingRangeRegistrationOptions{
				Boolean: new(true),
			},
			RenameProvider: &lsproto.BooleanOrRenameOptions{
				RenameOptions: &lsproto.RenameOptions{
					PrepareProvider: new(true),
				},
			},
			DocumentHighlightProvider: &lsproto.BooleanOrDocumentHighlightOptions{
				Boolean: new(true),
			},
			SelectionRangeProvider: &lsproto.BooleanOrSelectionRangeOptionsOrSelectionRangeRegistrationOptions{
				Boolean: new(true),
			},
			LinkedEditingRangeProvider: &lsproto.BooleanOrLinkedEditingRangeOptionsOrLinkedEditingRangeRegistrationOptions{
				Boolean: new(true),
			},
			InlayHintProvider: &lsproto.BooleanOrInlayHintOptionsOrInlayHintRegistrationOptions{
				Boolean: new(true),
			},
			CodeLensProvider: &lsproto.CodeLensOptions{
				ResolveProvider: new(true),
			},
			CodeActionProvider: &lsproto.BooleanOrCodeActionOptions{
				CodeActionOptions: &lsproto.CodeActionOptions{
					CodeActionKinds: &[]lsproto.CodeActionKind{
						lsproto.CodeActionKindQuickFix,
						lsproto.CodeActionKindSourceOrganizeImports,
						lsproto.CodeActionKindSourceRemoveUnusedImports,
						lsproto.CodeActionKindSourceSortImports,
						lsproto.CodeActionKindSourceFixAll,
					},
				},
			},
			CallHierarchyProvider: &lsproto.BooleanOrCallHierarchyOptionsOrCallHierarchyRegistrationOptions{
				Boolean: new(true),
			},
			Experimental: &lsproto.ExperimentalServerCapabilities{
				CustomSourceDefinitionProvider:       new(true),
				CustomMultiDocumentHighlightProvider: new(true),
			},
			VSReferencesProvider: new(true),
			VSOnAutoInsertProvider: &lsproto.VSOnAutoInsertOptions{
				VSTriggerCharacters: []string{">"},
			},
			Workspace: &lsproto.WorkspaceOptions{
				FileOperations: &lsproto.FileOperationOptions{
					WillRename: &lsproto.FileOperationRegistrationOptions{
						Filters: fileRenameFilters,
					},
				},
			},
			SemanticTokensProvider: &lsproto.SemanticTokensOptionsOrRegistrationOptions{
				Options: &lsproto.SemanticTokensOptions{
					Legend: ls.SemanticTokensLegend(s.clientCapabilities.TextDocument.SemanticTokens),
					Full: &lsproto.BooleanOrSemanticTokensFullDelta{
						Boolean: new(true),
					},
					Range: &lsproto.BooleanOrEmptyObject{
						Boolean: new(true),
					},
				},
			},
		},
	}

	return response, nil
}

func (s *Server) handleInitialized(ctx context.Context, params *lsproto.InitializedParams) error {
	var disablePushDiagnostics bool
	var enableTelemetry bool
	if s.initializationOptions.DisablePushDiagnostics != nil {
		disablePushDiagnostics = *s.initializationOptions.DisablePushDiagnostics
	}
	if s.initializationOptions.EnableTelemetry != nil {
		enableTelemetry = *s.initializationOptions.EnableTelemetry
	}
	hasDynamicWatchRegistration := s.clientCapabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration
	if hasDynamicWatchRegistration {
		s.logger.Logf("file watching: using LSP client-side watching (client supports dynamic registration)")
		s.watchEnabled = true
	} else if fswatch.Default().HasFastRecursiveBackend() {
		// The client cannot watch files itself, but the builtin watcher has a
		// backend with efficient recursive watching (Windows or FSEvents), so
		// fall back to watching files in-process.
		s.logger.Logf("file watching: using builtin in-process watcher (client lacks dynamic watch registration)")
		s.watchEnabled = true
		s.builtinWatcher = lspwatcher.New(s.fs, func(changes []*lsproto.FileEvent) {
			if s.session != nil {
				s.session.DidChangeWatchedFiles(s.backgroundCtx, changes)
			}
		}, s.logger)
	} else {
		// The client cannot watch files and the builtin watcher backend lacks
		// efficient recursive watching, so file watching is disabled.
		s.logger.Logf("file watching: disabled (client lacks dynamic watch registration and builtin watcher backend is not fast-recursive)")
	}

	cwd := s.cwd
	if s.clientCapabilities.Workspace.WorkspaceFolders &&
		s.initializeParams.WorkspaceFolders != nil &&
		s.initializeParams.WorkspaceFolders.WorkspaceFolders != nil &&
		len(*s.initializeParams.WorkspaceFolders.WorkspaceFolders) == 1 {
		cwd = lsproto.DocumentUri((*s.initializeParams.WorkspaceFolders.WorkspaceFolders)[0].Uri).FileName()
	} else if s.initializeParams.RootUri.DocumentUri != nil {
		cwd = s.initializeParams.RootUri.DocumentUri.FileName()
	} else if s.initializeParams.RootPath != nil && s.initializeParams.RootPath.String != nil {
		cwd = *s.initializeParams.RootPath.String
	}
	if !tspath.PathIsAbsolute(cwd) {
		cwd = s.cwd
	}

	s.telemetryEnabled = enableTelemetry

	s.session = project.NewSession(&project.SessionInit{
		BackgroundCtx: lsproto.WithClientCapabilities(s.backgroundCtx, &s.clientCapabilities),
		Options: &project.SessionOptions{
			CurrentDirectory:       cwd,
			DefaultLibraryPath:     s.defaultLibraryPath,
			TypingsLocation:        s.typingsLocation,
			PositionEncoding:       s.positionEncoding,
			WatchEnabled:           s.watchEnabled,
			LoggingEnabled:         true,
			TelemetryEnabled:       enableTelemetry,
			DebounceDelay:          500 * time.Millisecond,
			PushDiagnosticsEnabled: !disablePushDiagnostics,
			Locale:                 s.locale,
		},
		FS:          s.fs,
		Logger:      s.logger,
		Client:      s,
		NpmExecutor: s,
		ParseCache:  s.parseCache,
	})

	userPreferences, err := s.RequestConfiguration(ctx)
	if err != nil {
		return err
	}
	s.session.InitializeWithUserConfig(userPreferences)

	_, err = sendClientRequest(ctx, s, lsproto.ClientRegisterCapabilityInfo, &lsproto.RegistrationParams{
		Registrations: []*lsproto.Registration{
			{
				Id: "typescript-config-watch-id",
				RegisterOptions: &lsproto.RegisterOptions{
					WorkspaceDidChangeConfiguration: &lsproto.DidChangeConfigurationRegistrationOptions{
						Section: &lsproto.StringOrStrings{
							Strings: &[]string{"js/ts", "typescript", "javascript", "editor"},
						},
					},
				},
			},
		},
	})
	if err != nil {
		return fmt.Errorf("failed to register configuration change watcher: %w", err)
	}

	// !!! temporary.
	// Remove when we have `handleDidChangeConfiguration`/implicit project config support
	// derived from 'js/ts.implicitProjectConfig.*'.
	if s.compilerOptionsForInferredProjects != nil {
		s.session.DidChangeCompilerOptionsForInferredProjects(ctx, s.compilerOptionsForInferredProjects)
	}

	s.session.StartPerformanceTelemetry()

	close(s.initComplete)
	return nil
}

func (s *Server) handleShutdown(ctx context.Context, _ lsproto.NoParams, _ *lsproto.RequestMessage) (lsproto.ShutdownResponse, error) {
	if s.builtinWatcher != nil {
		s.builtinWatcher.Close()
	}
	s.session.Close()
	return lsproto.ShutdownResponse{}, nil
}

func (s *Server) handleExit(ctx context.Context, _ lsproto.NoParams) error {
	return io.EOF
}

func (s *Server) handleDidChangeWorkspaceConfiguration(ctx context.Context, params *lsproto.DidChangeConfigurationParams) error {
	if params.Settings == nil {
		return nil
	} else if settings, ok := params.Settings.(map[string]any); ok {
		s.session.Configure(lsutil.ParseUserPreferences(settings))
	}
	return nil
}

func (s *Server) handleDidOpen(ctx context.Context, params *lsproto.DidOpenTextDocumentParams) error {
	s.session.DidOpenFile(ctx, params.TextDocument.Uri, params.TextDocument.Version, params.TextDocument.Text, params.TextDocument.LanguageId)
	return nil
}

func (s *Server) handleDidChange(ctx context.Context, params *lsproto.DidChangeTextDocumentParams) error {
	s.session.DidChangeFile(ctx, params.TextDocument.Uri, params.TextDocument.Version, params.ContentChanges)
	return nil
}

func (s *Server) handleDidSave(ctx context.Context, params *lsproto.DidSaveTextDocumentParams) error {
	s.session.DidSaveFile(ctx, params.TextDocument.Uri)
	return nil
}

func (s *Server) handleDidClose(ctx context.Context, params *lsproto.DidCloseTextDocumentParams) error {
	s.session.DidCloseFile(ctx, params.TextDocument.Uri)
	return nil
}

func (s *Server) handleDidChangeWatchedFiles(ctx context.Context, params *lsproto.DidChangeWatchedFilesParams) error {
	s.session.DidChangeWatchedFiles(ctx, params.Changes)
	return nil
}

func (s *Server) handleSetTrace(_ context.Context, _ *lsproto.SetTraceParams) error {
	// $/setTrace is sent by vscode-languageclient when trace settings change.
	// Server log verbosity is controlled separately by custom/setLogVerbosity,
	// so this handler is intentionally a no-op.
	return nil
}

func (s *Server) handleSetLogVerbosity(_ context.Context, params *lsproto.SetLogVerbosityParams) error {
	if !isValidLogVerbosity(params.Verbosity) {
		return fmt.Errorf("%w: invalid log verbosity %d", lsproto.ErrorCodeInvalidParams, params.Verbosity)
	}
	s.logger.SetVerbosity(params.Verbosity)
	return nil
}

func (s *Server) handleDocumentDiagnostic(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentDiagnosticParams) (lsproto.DocumentDiagnosticResponse, error) {
	ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics)
	return ls.ProvideDiagnostics(ctx, params.TextDocument.Uri)
}

func (s *Server) handleHover(ctx context.Context, ls *ls.LanguageService, params *lsproto.HoverParams) (lsproto.HoverResponse, error) {
	return ls.ProvideHover(ctx, params)
}

func (s *Server) handlePrepareRename(ctx context.Context, languageService *ls.LanguageService, params *lsproto.PrepareRenameParams) (lsproto.PrepareRenameResponse, error) {
	info := languageService.GetRenameInfo(ctx, "" /*newName*/, params.TextDocument.Uri, params.Position)
	if !info.CanRename {
		return lsproto.PrepareRenameResponse{}, userFacingRequestFailedError(info.LocalizedErrorMessage)
	}
	return lsproto.PrepareRenameResponse{
		PrepareRenamePlaceholder: &lsproto.PrepareRenamePlaceholder{
			Range:       info.TriggerSpan,
			Placeholder: info.DisplayName,
		},
	}, nil
}

func (s *Server) handleRename(ctx context.Context, params *lsproto.RenameParams, req *lsproto.RequestMessage) (lsproto.RenameResponse, error) {
	defaultLs, orchestrator, err := s.getLanguageServiceAndCrossProjectOrchestrator(ctx, params.TextDocument.Uri, req)
	if err != nil {
		return lsproto.RenameResponse{}, err
	}
	info := defaultLs.GetRenameInfo(ctx, params.NewName, params.TextDocument.Uri, params.Position)
	if info.CanRename && info.FileToRename != "" {
		// We send a `willRenameFiles` request if the client allows;
		// otherwise we directly compute the edits for renaming the file.
		if ls.ClientSupportsWillRenameFiles(ctx) {
			documentChanges := []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile{
				{
					RenameFile: &lsproto.RenameFile{
						Kind:   lsproto.StringLiteralRename{},
						OldUri: lsconv.FileNameToDocumentURI(info.FileToRename),
						NewUri: lsconv.FileNameToDocumentURI(info.NewFileName),
					},
				},
			}
			return lsproto.WorkspaceEditOrNull{
				WorkspaceEdit: &lsproto.WorkspaceEdit{
					DocumentChanges: &documentChanges,
				},
			}, nil
		}
		renameFilesParams := &lsproto.RenameFilesParams{
			Files: []*lsproto.FileRename{{
				OldUri: string(lsconv.FileNameToDocumentURI(info.FileToRename)),
				NewUri: string(lsconv.FileNameToDocumentURI(info.NewFileName)),
			}},
		}
		return s.handleWillRenameFilesWorker(ctx, renameFilesParams, req, true /*sendRenameFile*/)
	}

	return defaultLs.ProvideRename(ctx, params, orchestrator)
}

func (s *Server) handleWillRenameFiles(ctx context.Context, params *lsproto.RenameFilesParams, msg *lsproto.RequestMessage) (lsproto.WillRenameFilesResponse, error) {
	return s.handleWillRenameFilesWorker(ctx, params, msg, false /*sendRenameFile*/)
}

// If `sendRenameFile` is true, the original `willRenameFiles` request is being handled as part of a rename operation
// where the client doesn't support `willRenameFiles`,
// so we should include the file rename in the edits we return
func (s *Server) handleWillRenameFilesWorker(ctx context.Context, params *lsproto.RenameFilesParams, _ *lsproto.RequestMessage, sendRenameFile bool) (lsproto.WillRenameFilesResponse, error) {
	if len(params.Files) == 0 {
		return lsproto.WillRenameFilesResponse{}, nil
	}

	uris := make([]lsproto.DocumentUri, 0, len(params.Files))
	for _, file := range params.Files {
		uris = append(uris, lsproto.DocumentUri(file.OldUri))
	}

	if len(uris) == 0 {
		return lsproto.WillRenameFilesResponse{}, nil
	}

	services := s.session.GetLanguageServicesForDocuments(ctx, uris)

	type editKey struct {
		uri    lsproto.DocumentUri
		range_ lsproto.Range
	}
	seenEdits := make(map[editKey]string)
	seenRenames := make(map[lsproto.DocumentUri]bool)
	var documentChanges []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile

	for _, languageService := range services {
		for _, file := range params.Files {
			changes := languageService.GetEditsForFileRename(ctx, lsproto.DocumentUri(file.OldUri), lsproto.DocumentUri(file.NewUri))
			for _, change := range changes {
				if change.RenameFile != nil {
					if !seenRenames[change.RenameFile.OldUri] {
						seenRenames[change.RenameFile.OldUri] = true
						documentChanges = append(documentChanges, change)
					}
				} else if change.TextDocumentEdit != nil {
					uri := change.TextDocumentEdit.TextDocument.Uri
					var deduped []lsproto.TextEditOrAnnotatedTextEditOrSnippetTextEdit
					for _, edit := range change.TextDocumentEdit.Edits {
						if edit.TextEdit != nil {
							key := editKey{uri: uri, range_: edit.TextEdit.Range}
							if prev, ok := seenEdits[key]; ok && prev == edit.TextEdit.NewText {
								continue
							}
							seenEdits[key] = edit.TextEdit.NewText
						}
						deduped = append(deduped, edit)
					}
					if len(deduped) > 0 {
						documentChanges = append(documentChanges, lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile{
							TextDocumentEdit: &lsproto.TextDocumentEdit{
								TextDocument: change.TextDocumentEdit.TextDocument,
								Edits:        deduped,
							},
						})
					}
				}
			}
		}
	}

	if sendRenameFile {
		for _, file := range params.Files {
			documentChanges = append(documentChanges, lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile{
				RenameFile: &lsproto.RenameFile{
					Kind:   lsproto.StringLiteralRename{},
					OldUri: lsproto.DocumentUri(file.OldUri),
					NewUri: lsproto.DocumentUri(file.NewUri),
				},
			})
		}
	}

	if len(documentChanges) == 0 {
		return lsproto.WillRenameFilesResponse{}, nil
	}

	if ls.ClientSupportsDocumentChanges(ctx) {
		return lsproto.WillRenameFilesResponse{
			WorkspaceEdit: &lsproto.WorkspaceEdit{
				DocumentChanges: &documentChanges,
			},
		}, nil
	}

	changes := make(map[lsproto.DocumentUri][]*lsproto.TextEdit)
	for _, change := range documentChanges {
		if change.TextDocumentEdit != nil {
			uri := change.TextDocumentEdit.TextDocument.Uri
			for _, edit := range change.TextDocumentEdit.Edits {
				if edit.TextEdit != nil {
					changes[uri] = append(changes[uri], edit.TextEdit)
				}
			}
		}
	}

	return lsproto.WillRenameFilesResponse{
		WorkspaceEdit: &lsproto.WorkspaceEdit{
			Changes: new(changes),
		},
	}, nil
}

func (s *Server) handleSignatureHelp(ctx context.Context, languageService *ls.LanguageService, params *lsproto.SignatureHelpParams) (lsproto.SignatureHelpResponse, error) {
	return languageService.ProvideSignatureHelp(
		ctx,
		params.TextDocument.Uri,
		params.Position,
		params.Context,
	)
}

func (s *Server) handleFoldingRange(ctx context.Context, ls *ls.LanguageService, params *lsproto.FoldingRangeParams) (lsproto.FoldingRangeResponse, error) {
	return ls.ProvideFoldingRange(ctx, params.TextDocument.Uri)
}

func (s *Server) handleVSOnAutoInsert(ctx context.Context, ls *ls.LanguageService, params *lsproto.VSOnAutoInsertParams) (lsproto.VSOnAutoInsertResponse, error) {
	return ls.ProvideOnAutoInsert(ctx, params)
}

func (s *Server) handleLinkedEditingRange(ctx context.Context, ls *ls.LanguageService, params *lsproto.LinkedEditingRangeParams) (lsproto.LinkedEditingRangeResponse, error) {
	return ls.ProvideLinkedEditingRange(ctx, params)
}

func (s *Server) handleDefinition(ctx context.Context, ls *ls.LanguageService, params *lsproto.DefinitionParams) (lsproto.DefinitionResponse, error) {
	return ls.ProvideDefinition(ctx, params.TextDocument.Uri, params.Position)
}

func (s *Server) handleSourceDefinition(ctx context.Context, ls *ls.LanguageService, params *lsproto.TextDocumentPositionParams) (lsproto.CustomTextDocumentSourceDefinitionResponse, error) {
	resp, err := ls.ProvideSourceDefinition(ctx, params.TextDocument.Uri, params.Position)
	if err != nil {
		return nil, err
	}
	return &resp, nil
}

func (s *Server) handleTypeDefinition(ctx context.Context, ls *ls.LanguageService, params *lsproto.TypeDefinitionParams) (lsproto.TypeDefinitionResponse, error) {
	return ls.ProvideTypeDefinition(ctx, params.TextDocument.Uri, params.Position)
}

func (s *Server) handleCompletion(ctx context.Context, languageService *ls.LanguageService, params *lsproto.CompletionParams) (lsproto.CompletionResponse, error) {
	return languageService.ProvideCompletion(
		ctx,
		params.TextDocument.Uri,
		params.Position,
		params.Context,
	)
}

func (s *Server) handleCompletionItemResolve(ctx context.Context, params *lsproto.CompletionItem, reqMsg *lsproto.RequestMessage) (lsproto.CompletionResolveResponse, error) {
	data := params.Data
	languageService, err := s.session.GetLanguageService(ctx, lsconv.FileNameToDocumentURI(data.FileName))
	if err != nil {
		return nil, err
	}
	defer s.recover(reqMsg)
	return languageService.ResolveCompletionItem(
		ctx,
		params,
		data,
	)
}

func (s *Server) handleDocumentFormat(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentFormattingParams) (lsproto.DocumentFormattingResponse, error) {
	return ls.ProvideFormatDocument(
		ctx,
		params.TextDocument.Uri,
		params.Options,
	)
}

func (s *Server) handleDocumentRangeFormat(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentRangeFormattingParams) (lsproto.DocumentRangeFormattingResponse, error) {
	return ls.ProvideFormatDocumentRange(
		ctx,
		params.TextDocument.Uri,
		params.Options,
		params.Range,
	)
}

func (s *Server) handleDocumentOnTypeFormat(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentOnTypeFormattingParams) (lsproto.DocumentOnTypeFormattingResponse, error) {
	return ls.ProvideFormatDocumentOnType(
		ctx,
		params.TextDocument.Uri,
		params.Options,
		params.Position,
		params.Ch,
	)
}

func (s *Server) handleWorkspaceSymbol(ctx context.Context, params *lsproto.WorkspaceSymbolParams, reqMsg *lsproto.RequestMessage) (lsproto.WorkspaceSymbolResponse, error) {
	var resp lsproto.WorkspaceSymbolResponse
	var lsErr error
	s.session.WithSnapshotLoadingProjectTree(ctx, nil, func(snapshot *project.Snapshot) {
		defer s.recover(reqMsg)
		programs := core.Map(snapshot.ProjectCollection.Projects(), (*project.Project).GetProgram)
		resp, lsErr = ls.ProvideWorkspaceSymbols(
			ctx,
			programs,
			snapshot.Converters(),
			snapshot.UserPreferences(),
			params.Query,
		)
	})
	return resp, lsErr
}

func (s *Server) handleDocumentSymbol(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentSymbolParams) (lsproto.DocumentSymbolResponse, error) {
	return ls.ProvideDocumentSymbols(ctx, params.TextDocument.Uri)
}

func (s *Server) handleDocumentHighlight(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentHighlightParams) (lsproto.DocumentHighlightResponse, error) {
	return ls.ProvideDocumentHighlights(ctx, params.TextDocument.Uri, params.Position)
}

func (s *Server) handleMultiDocumentHighlight(ctx context.Context, ls *ls.LanguageService, params *lsproto.MultiDocumentHighlightParams) (lsproto.CustomMultiDocumentHighlightResponse, error) {
	return ls.ProvideMultiDocumentHighlights(ctx, params.TextDocument.Uri, params.Position, params.FilesToSearch)
}

func (s *Server) handleSelectionRange(ctx context.Context, ls *ls.LanguageService, params *lsproto.SelectionRangeParams) (lsproto.SelectionRangeResponse, error) {
	return ls.ProvideSelectionRanges(ctx, params)
}

func (s *Server) handleCodeAction(ctx context.Context, ls *ls.LanguageService, params *lsproto.CodeActionParams) (lsproto.CodeActionResponse, error) {
	return ls.ProvideCodeActions(ctx, params)
}

func (s *Server) handleInlayHint(
	ctx context.Context,
	languageService *ls.LanguageService,
	params *lsproto.InlayHintParams,
) (lsproto.InlayHintResponse, error) {
	return languageService.ProvideInlayHint(ctx, params)
}

func (s *Server) handleCodeLens(ctx context.Context, ls *ls.LanguageService, params *lsproto.CodeLensParams) (lsproto.CodeLensResponse, error) {
	return ls.ProvideCodeLenses(ctx, params.TextDocument.Uri)
}

func (s *Server) handleCodeLensResolve(ctx context.Context, codeLens *lsproto.CodeLens, reqMsg *lsproto.RequestMessage) (*lsproto.CodeLens, error) {
	defaultLs, orchestrator, err := s.getLanguageServiceAndCrossProjectOrchestrator(ctx, codeLens.Data.Uri, reqMsg)
	if ctx.Err() != nil {
		return nil, ctx.Err()
	}
	if err != nil {
		// This can happen if a codeLens/resolve request comes in after a program change.
		// While it's true that handlers should latch onto a specific snapshot
		// while processing requests, we just set `Data.Uri` based on
		// some older snapshot's contents. The content could have been modified,
		// or the file itself could have been removed from the session entirely.
		// Note this won't bail out on every change, but will prevent crashing
		// based on non-existent files and line maps from shortened files.
		return codeLens, lsproto.ErrorCodeContentModified
	}
	defer s.recover(reqMsg)
	return defaultLs.ResolveCodeLens(
		ctx,
		codeLens,
		s.initializationOptions.CodeLensShowLocationsCommandName,
		orchestrator,
	)
}

func (s *Server) handlePrepareCallHierarchy(
	ctx context.Context,
	languageService *ls.LanguageService,
	params *lsproto.CallHierarchyPrepareParams,
) (lsproto.CallHierarchyPrepareResponse, error) {
	return languageService.ProvidePrepareCallHierarchy(ctx, params.TextDocument.Uri, params.Position)
}

func (s *Server) handleCallHierarchyIncomingCalls(
	ctx context.Context,
	params *lsproto.CallHierarchyIncomingCallsParams,
	reqMsg *lsproto.RequestMessage,
) (lsproto.CallHierarchyIncomingCallsResponse, error) {
	defaultLs, orchestrator, err := s.getLanguageServiceAndCrossProjectOrchestrator(ctx, params.Item.Uri, reqMsg)
	if err != nil {
		return lsproto.CallHierarchyIncomingCallsOrNull{}, err
	}
	return defaultLs.ProvideCallHierarchyIncomingCalls(ctx, params.Item, orchestrator)
}

func (s *Server) handleCallHierarchyOutgoingCalls(
	ctx context.Context,
	params *lsproto.CallHierarchyOutgoingCallsParams,
	_ *lsproto.RequestMessage,
) (lsproto.CallHierarchyOutgoingCallsResponse, error) {
	languageService, err := s.session.GetLanguageService(ctx, params.Item.Uri)
	if err != nil {
		return lsproto.CallHierarchyOutgoingCallsOrNull{}, err
	}
	return languageService.ProvideCallHierarchyOutgoingCalls(ctx, params.Item)
}

func (s *Server) handleSemanticTokensFull(ctx context.Context, ls *ls.LanguageService, params *lsproto.SemanticTokensParams) (lsproto.SemanticTokensResponse, error) {
	return ls.ProvideSemanticTokens(ctx, params.TextDocument.Uri)
}

func (s *Server) handleSemanticTokensRange(ctx context.Context, ls *ls.LanguageService, params *lsproto.SemanticTokensRangeParams) (lsproto.SemanticTokensRangeResponse, error) {
	return ls.ProvideSemanticTokensRange(ctx, params.TextDocument.Uri, params.Range)
}

func (s *Server) handleInitializeAPISession(ctx context.Context, params *lsproto.InitializeAPISessionParams, _ *lsproto.RequestMessage) (lsproto.CustomInitializeAPISessionResponse, error) {
	s.apiSessionsMu.Lock()
	defer s.apiSessionsMu.Unlock()

	if s.apiSessions == nil {
		s.apiSessions = make(map[string]*api.Session)
	}

	var apiSession *api.Session
	apiSession = api.NewSession(s.session, nil)

	// Use provided pipe path or generate a unique one
	var pipePath string
	if params.Pipe != nil && *params.Pipe != "" {
		pipePath = *params.Pipe
	} else {
		pipePath = s.generateAPIPipePath()
	}

	transport, err := api.NewPipeTransport(pipePath)
	if err != nil {
		return nil, fmt.Errorf("failed to create API transport: %w", err)
	}

	// Start accepting connections in the background
	go func() {
		defer func() {
			apiSession.Close()
			s.removeAPISession(apiSession.ID())
		}()

		rwc, acceptErr := transport.Accept()
		_ = transport.Close()
		if acceptErr != nil {
			s.logger.Errorf("API session %s: failed to accept connection: %v", apiSession.ID(), acceptErr)
			return
		}

		// Create a cancellable context for the API connection
		apiCtx, apiCancel := context.WithCancel(s.backgroundCtx)
		defer apiCancel()

		// Run the connection with panic recovery
		defer func() {
			if r := recover(); r != nil {
				stack := debug.Stack()
				s.logger.Errorf("API session %s: panic: %v\n%s", apiSession.ID(), r, string(stack))
				// Cancel the context to shut down the connection
				apiCancel()
				// Close the underlying connection
				rwc.Close()
			}
		}()

		conn := api.NewAsyncConn(rwc, apiSession)
		if apiErr := conn.Run(apiCtx); apiErr != nil {
			s.logger.Errorf("API session %s: %v", apiSession.ID(), apiErr)
		}
	}()

	s.apiSessions[apiSession.ID()] = apiSession

	return &lsproto.InitializeAPISessionResult{
		SessionId: apiSession.ID(),
		Pipe:      pipePath,
	}, nil
}

func (s *Server) generateAPIPipePath() string {
	// Generate a high-entropy path using time and random source
	now := time.Now().UnixNano()
	rnd := rand.Uint64()
	return api.GeneratePipePath(fmt.Sprintf("tsgo-api-%x-%x", now, rnd))
}

func (s *Server) removeAPISession(id string) {
	s.apiSessionsMu.Lock()
	defer s.apiSessionsMu.Unlock()
	delete(s.apiSessions, id)
}

// !!! temporary; remove when we have `handleDidChangeConfiguration`/implicit project config support
func (s *Server) SetCompilerOptionsForInferredProjects(ctx context.Context, options *core.CompilerOptions) {
	s.compilerOptionsForInferredProjects = options
	if s.session != nil {
		s.session.DidChangeCompilerOptionsForInferredProjects(ctx, options)
	}
}

// NpmInstall implements ata.NpmExecutor
func (s *Server) NpmInstall(cwd string, args []string) ([]byte, error) {
	return s.npmInstall(cwd, args)
}

// Developer/debugging command handlers

func (s *Server) handleRunGC(_ context.Context, _ lsproto.NoParams, _ *lsproto.RequestMessage) (lsproto.RunGCResponse, error) {
	pprof.RunGC()
	s.logger.Info("GC triggered")
	return lsproto.Null{}, nil
}

func (s *Server) handleSaveHeapProfile(_ context.Context, params *lsproto.ProfileParams, _ *lsproto.RequestMessage) (*lsproto.ProfileResult, error) {
	filePath, err := pprof.SaveHeapProfile(params.Dir)
	if err != nil {
		return nil, err
	}
	s.logger.Info("Heap profile saved to: ", filePath)
	return &lsproto.ProfileResult{File: filePath}, nil
}

func (s *Server) handleSaveAllocProfile(_ context.Context, params *lsproto.ProfileParams, _ *lsproto.RequestMessage) (*lsproto.ProfileResult, error) {
	filePath, err := pprof.SaveAllocProfile(params.Dir)
	if err != nil {
		return nil, err
	}
	s.logger.Info("Allocation profile saved to: ", filePath)
	return &lsproto.ProfileResult{File: filePath}, nil
}

func (s *Server) handleStartCPUProfile(_ context.Context, params *lsproto.ProfileParams, _ *lsproto.RequestMessage) (lsproto.StartCPUProfileResponse, error) {
	err := s.cpuProfiler.StartCPUProfile(params.Dir)
	if err != nil {
		return lsproto.Null{}, err
	}
	s.logger.Info("CPU profiling started, will save to: ", params.Dir)
	return lsproto.Null{}, nil
}

func (s *Server) handleStopCPUProfile(_ context.Context, _ lsproto.NoParams, _ *lsproto.RequestMessage) (*lsproto.ProfileResult, error) {
	filePath, err := s.cpuProfiler.StopCPUProfile()
	if err != nil {
		return nil, err
	}
	s.logger.Info("CPU profile saved to: ", filePath)
	return &lsproto.ProfileResult{File: filePath}, nil
}

func (s *Server) handleProjectInfo(ctx context.Context, params *lsproto.ProjectInfoParams, _ *lsproto.RequestMessage) (lsproto.CustomProjectInfoResponse, error) {
	uri := params.TextDocument.Uri
	defaultProject, _, _, err := s.session.GetLanguageServiceAndProjectsForFile(ctx, uri)
	if err != nil {
		return nil, err
	}
	configFilePath := ""
	if defaultProject != nil && defaultProject.Kind == project.KindConfigured {
		configFilePath = defaultProject.Name()
	}
	return &lsproto.ProjectInfoResult{
		ConfigFilePath: configFilePath,
	}, nil
}
