package watcher

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/bmatcuk/doublestar/v4"
	"github.com/fsnotify/fsnotify"
	"github.com/opencode-ai/opencode/internal/config"
	"github.com/opencode-ai/opencode/internal/lsp"
	"github.com/opencode-ai/opencode/internal/lsp/protocol"
	"log/slog"
)

// WorkspaceWatcher manages LSP file watching
type WorkspaceWatcher struct {
	client        *lsp.Client
	workspacePath string

	debounceTime time.Duration
	debounceMap  map[string]*time.Timer
	debounceMu   sync.Mutex

	// File watchers registered by the server
	registrations  []protocol.FileSystemWatcher
	registrationMu sync.RWMutex
}

// NewWorkspaceWatcher creates a new workspace watcher
func NewWorkspaceWatcher(client *lsp.Client) *WorkspaceWatcher {
	return &WorkspaceWatcher{
		client:        client,
		debounceTime:  300 * time.Millisecond,
		debounceMap:   make(map[string]*time.Timer),
		registrations: []protocol.FileSystemWatcher{},
	}
}

// AddRegistrations adds file watchers to track
func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watchers []protocol.FileSystemWatcher) {
	cnf := config.Get()

	slog.Debug("Adding file watcher registrations")
	w.registrationMu.Lock()
	defer w.registrationMu.Unlock()

	// Add new watchers
	w.registrations = append(w.registrations, watchers...)

	// Print detailed registration information for debugging
	if cnf.DebugLSP {
		slog.Debug("Adding file watcher registrations",
			"id", id,
			"watchers", len(watchers),
			"total", len(w.registrations),
		)

		for i, watcher := range watchers {
			slog.Debug("Registration", "index", i+1)

			// Log the GlobPattern
			switch v := watcher.GlobPattern.Value.(type) {
			case string:
				slog.Debug("GlobPattern", "pattern", v)
			case protocol.RelativePattern:
				slog.Debug("GlobPattern", "pattern", v.Pattern)

				// Log BaseURI details
				switch u := v.BaseURI.Value.(type) {
				case string:
					slog.Debug("BaseURI", "baseURI", u)
				case protocol.DocumentUri:
					slog.Debug("BaseURI", "baseURI", u)
				default:
					slog.Debug("BaseURI", "baseURI", u)
				}
			default:
				slog.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v))
			}

			// Log WatchKind
			watchKind := protocol.WatchKind(protocol.WatchChange | protocol.WatchCreate | protocol.WatchDelete)
			if watcher.Kind != nil {
				watchKind = *watcher.Kind
			}

			slog.Debug("WatchKind", "kind", watchKind)
		}
	}

	// Determine server type for specialized handling
	serverName := getServerNameFromContext(ctx)
	slog.Debug("Server type detected", "serverName", serverName)

	// Check if this server has sent file watchers
	hasFileWatchers := len(watchers) > 0

	// For servers that need file preloading, we'll use a smart approach
	if shouldPreloadFiles(serverName) || !hasFileWatchers {
		go func() {
			startTime := time.Now()
			filesOpened := 0

			// Determine max files to open based on server type
			maxFilesToOpen := 50 // Default conservative limit

			switch serverName {
			case "typescript", "typescript-language-server", "tsserver", "vtsls":
				// TypeScript servers benefit from seeing more files
				maxFilesToOpen = 100
			case "java", "jdtls":
				// Java servers need to see many files for project model
				maxFilesToOpen = 200
			}

			// First, open high-priority files
			highPriorityFilesOpened := w.openHighPriorityFiles(ctx, serverName)
			filesOpened += highPriorityFilesOpened

			if cnf.DebugLSP {
				slog.Debug("Opened high-priority files",
					"count", highPriorityFilesOpened,
					"serverName", serverName)
			}

			// If we've already opened enough high-priority files, we might not need more
			if filesOpened >= maxFilesToOpen {
				if cnf.DebugLSP {
					slog.Debug("Reached file limit with high-priority files",
						"filesOpened", filesOpened,
						"maxFiles", maxFilesToOpen)
				}
				return
			}

			// For the remaining slots, walk the directory and open matching files

			err := filepath.WalkDir(w.workspacePath, func(path string, d os.DirEntry, err error) error {
				if err != nil {
					return err
				}

				// Skip directories that should be excluded
				if d.IsDir() {
					if path != w.workspacePath && shouldExcludeDir(path) {
						if cnf.DebugLSP {
							slog.Debug("Skipping excluded directory", "path", path)
						}
						return filepath.SkipDir
					}
				} else {
					// Process files, but limit the total number
					if filesOpened < maxFilesToOpen {
						// Only process if it's not already open (high-priority files were opened earlier)
						if !w.client.IsFileOpen(path) {
							w.openMatchingFile(ctx, path)
							filesOpened++

							// Add a small delay after every 10 files to prevent overwhelming the server
							if filesOpened%10 == 0 {
								time.Sleep(50 * time.Millisecond)
							}
						}
					} else {
						// We've reached our limit, stop walking
						return filepath.SkipAll
					}
				}

				return nil
			})

			elapsedTime := time.Since(startTime)
			if cnf.DebugLSP {
				slog.Debug("Limited workspace scan complete",
					"filesOpened", filesOpened,
					"maxFiles", maxFilesToOpen,
					"elapsedTime", elapsedTime.Seconds(),
					"workspacePath", w.workspacePath,
				)
			}

			if err != nil && cnf.DebugLSP {
				slog.Debug("Error scanning workspace for files to open", "error", err)
			}
		}()
	} else if cnf.DebugLSP {
		slog.Debug("Using on-demand file loading for server", "server", serverName)
	}
}

// openHighPriorityFiles opens important files for the server type
// Returns the number of files opened
func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName string) int {
	cnf := config.Get()
	filesOpened := 0

	// Define patterns for high-priority files based on server type
	var patterns []string

	switch serverName {
	case "typescript", "typescript-language-server", "tsserver", "vtsls":
		patterns = []string{
			"**/tsconfig.json",
			"**/package.json",
			"**/jsconfig.json",
			"**/index.ts",
			"**/index.js",
			"**/main.ts",
			"**/main.js",
		}
	case "gopls":
		patterns = []string{
			"**/go.mod",
			"**/go.sum",
			"**/main.go",
		}
	case "rust-analyzer":
		patterns = []string{
			"**/Cargo.toml",
			"**/Cargo.lock",
			"**/src/lib.rs",
			"**/src/main.rs",
		}
	case "python", "pyright", "pylsp":
		patterns = []string{
			"**/pyproject.toml",
			"**/setup.py",
			"**/requirements.txt",
			"**/__init__.py",
			"**/__main__.py",
		}
	case "clangd":
		patterns = []string{
			"**/CMakeLists.txt",
			"**/Makefile",
			"**/compile_commands.json",
		}
	case "java", "jdtls":
		patterns = []string{
			"**/pom.xml",
			"**/build.gradle",
			"**/src/main/java/**/*.java",
		}
	default:
		// For unknown servers, use common configuration files
		patterns = []string{
			"**/package.json",
			"**/Makefile",
			"**/CMakeLists.txt",
			"**/.editorconfig",
		}
	}

	// For each pattern, find and open matching files
	for _, pattern := range patterns {
		// Use doublestar.Glob to find files matching the pattern (supports ** patterns)
		matches, err := doublestar.Glob(os.DirFS(w.workspacePath), pattern)
		if err != nil {
			if cnf.DebugLSP {
				slog.Debug("Error finding high-priority files", "pattern", pattern, "error", err)
			}
			continue
		}

		for _, match := range matches {
			// Convert relative path to absolute
			fullPath := filepath.Join(w.workspacePath, match)

			// Skip directories and excluded files
			info, err := os.Stat(fullPath)
			if err != nil || info.IsDir() || shouldExcludeFile(fullPath) {
				continue
			}

			// Open the file
			if err := w.client.OpenFile(ctx, fullPath); err != nil {
				if cnf.DebugLSP {
					slog.Debug("Error opening high-priority file", "path", fullPath, "error", err)
				}
			} else {
				filesOpened++
				if cnf.DebugLSP {
					slog.Debug("Opened high-priority file", "path", fullPath)
				}
			}

			// Add a small delay to prevent overwhelming the server
			time.Sleep(20 * time.Millisecond)

			// Limit the number of files opened per pattern
			if filesOpened >= 5 && (serverName != "java" && serverName != "jdtls") {
				break
			}
		}
	}

	return filesOpened
}

// WatchWorkspace sets up file watching for a workspace
func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath string) {
	cnf := config.Get()
	w.workspacePath = workspacePath

	// Store the watcher in the context for later use
	ctx = context.WithValue(ctx, "workspaceWatcher", w)

	// If the server name isn't already in the context, try to detect it
	if _, ok := ctx.Value("serverName").(string); !ok {
		serverName := getServerNameFromContext(ctx)
		ctx = context.WithValue(ctx, "serverName", serverName)
	}

	serverName := getServerNameFromContext(ctx)
	slog.Debug("Starting workspace watcher", "workspacePath", workspacePath, "serverName", serverName)

	// Register handler for file watcher registrations from the server
	lsp.RegisterFileWatchHandler(func(id string, watchers []protocol.FileSystemWatcher) {
		w.AddRegistrations(ctx, id, watchers)
	})

	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		slog.Error("Error creating watcher", "error", err)
	}
	defer watcher.Close()

	// Watch the workspace recursively
	err = filepath.WalkDir(workspacePath, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			return err
		}

		// Skip excluded directories (except workspace root)
		if d.IsDir() && path != workspacePath {
			if shouldExcludeDir(path) {
				if cnf.DebugLSP {
					slog.Debug("Skipping excluded directory", "path", path)
				}
				return filepath.SkipDir
			}
		}

		// Add directories to watcher
		if d.IsDir() {
			err = watcher.Add(path)
			if err != nil {
				slog.Error("Error watching path", "path", path, "error", err)
			}
		}

		return nil
	})
	if err != nil {
		slog.Error("Error walking workspace", "error", err)
	}

	// Event loop
	for {
		select {
		case <-ctx.Done():
			return
		case event, ok := <-watcher.Events:
			if !ok {
				return
			}

			uri := fmt.Sprintf("file://%s", event.Name)

			// Add new directories to the watcher
			if event.Op&fsnotify.Create != 0 {
				// Check if the file/directory still exists before processing
				info, err := os.Stat(event.Name)
				if err != nil {
					if os.IsNotExist(err) {
						// File was deleted between event and processing - ignore
						slog.Debug("File deleted between create event and stat", "path", event.Name)
						continue
					}
					slog.Error("Error getting file info", "path", event.Name, "error", err)
					continue
				}

				if info.IsDir() {
					// Skip excluded directories
					if !shouldExcludeDir(event.Name) {
						if err := watcher.Add(event.Name); err != nil {
							slog.Error("Error adding directory to watcher", "path", event.Name, "error", err)
						}
					}
				} else {
					// For newly created files
					if !shouldExcludeFile(event.Name) {
						w.openMatchingFile(ctx, event.Name)
					}
				}
			}

			// Debug logging
			if cnf.DebugLSP {
				matched, kind := w.isPathWatched(event.Name)
				slog.Debug("File event",
					"path", event.Name,
					"operation", event.Op.String(),
					"watched", matched,
					"kind", kind,
				)

			}

			// Check if this path should be watched according to server registrations
			if watched, watchKind := w.isPathWatched(event.Name); watched {
				switch {
				case event.Op&fsnotify.Write != 0:
					if watchKind&protocol.WatchChange != 0 {
						w.debounceHandleFileEvent(ctx, uri, protocol.FileChangeType(protocol.Changed))
					}
				case event.Op&fsnotify.Create != 0:
					// Already handled earlier in the event loop
					// Just send the notification if needed
					info, err := os.Stat(event.Name)
					if err != nil {
						slog.Error("Error getting file info", "path", event.Name, "error", err)
						return
					}
					if !info.IsDir() && watchKind&protocol.WatchCreate != 0 {
						w.debounceHandleFileEvent(ctx, uri, protocol.FileChangeType(protocol.Created))
					}
				case event.Op&fsnotify.Remove != 0:
					if watchKind&protocol.WatchDelete != 0 {
						w.handleFileEvent(ctx, uri, protocol.FileChangeType(protocol.Deleted))
					}
				case event.Op&fsnotify.Rename != 0:
					// For renames, first delete
					if watchKind&protocol.WatchDelete != 0 {
						w.handleFileEvent(ctx, uri, protocol.FileChangeType(protocol.Deleted))
					}

					// Then check if the new file exists and create an event
					if info, err := os.Stat(event.Name); err == nil && !info.IsDir() {
						if watchKind&protocol.WatchCreate != 0 {
							w.debounceHandleFileEvent(ctx, uri, protocol.FileChangeType(protocol.Created))
						}
					}
				}
			}
		case err, ok := <-watcher.Errors:
			if !ok {
				return
			}
			slog.Error("Error watching file", "error", err)
		}
	}
}

// isPathWatched checks if a path should be watched based on server registrations
func (w *WorkspaceWatcher) isPathWatched(path string) (bool, protocol.WatchKind) {
	w.registrationMu.RLock()
	defer w.registrationMu.RUnlock()

	// If no explicit registrations, watch everything
	if len(w.registrations) == 0 {
		return true, protocol.WatchKind(protocol.WatchChange | protocol.WatchCreate | protocol.WatchDelete)
	}

	// Check each registration
	for _, reg := range w.registrations {
		isMatch := w.matchesPattern(path, reg.GlobPattern)
		if isMatch {
			kind := protocol.WatchKind(protocol.WatchChange | protocol.WatchCreate | protocol.WatchDelete)
			if reg.Kind != nil {
				kind = *reg.Kind
			}
			return true, kind
		}
	}

	return false, 0
}

// matchesGlob handles advanced glob patterns including ** and alternatives
func matchesGlob(pattern, path string) bool {
	// Handle file extension patterns with braces like *.{go,mod,sum}
	if strings.Contains(pattern, "{") && strings.Contains(pattern, "}") {
		// Extract extensions from pattern like "*.{go,mod,sum}"
		parts := strings.SplitN(pattern, "{", 2)
		if len(parts) == 2 {
			prefix := parts[0]
			extPart := strings.SplitN(parts[1], "}", 2)
			if len(extPart) == 2 {
				extensions := strings.Split(extPart[0], ",")
				suffix := extPart[1]

				// Check if the path matches any of the extensions
				for _, ext := range extensions {
					extPattern := prefix + ext + suffix
					isMatch := matchesSimpleGlob(extPattern, path)
					if isMatch {
						return true
					}
				}
				return false
			}
		}
	}

	return matchesSimpleGlob(pattern, path)
}

// matchesSimpleGlob handles glob patterns with ** wildcards
func matchesSimpleGlob(pattern, path string) bool {
	// Handle special case for **/*.ext pattern (common in LSP)
	if strings.HasPrefix(pattern, "**/") {
		rest := strings.TrimPrefix(pattern, "**/")

		// If the rest is a simple file extension pattern like *.go
		if strings.HasPrefix(rest, "*.") {
			ext := strings.TrimPrefix(rest, "*")
			isMatch := strings.HasSuffix(path, ext)
			return isMatch
		}

		// Otherwise, try to check if the path ends with the rest part
		isMatch := strings.HasSuffix(path, rest)

		// If it matches directly, great!
		if isMatch {
			return true
		}

		// Otherwise, check if any path component matches
		pathComponents := strings.Split(path, "/")
		for i := range pathComponents {
			subPath := strings.Join(pathComponents[i:], "/")
			if strings.HasSuffix(subPath, rest) {
				return true
			}
		}

		return false
	}

	// Handle other ** wildcard pattern cases
	if strings.Contains(pattern, "**") {
		parts := strings.Split(pattern, "**")

		// Validate the path starts with the first part
		if !strings.HasPrefix(path, parts[0]) && parts[0] != "" {
			return false
		}

		// For patterns like "**/*.go", just check the suffix
		if len(parts) == 2 && parts[0] == "" {
			isMatch := strings.HasSuffix(path, parts[1])
			return isMatch
		}

		// For other patterns, handle middle part
		remaining := strings.TrimPrefix(path, parts[0])
		if len(parts) == 2 {
			isMatch := strings.HasSuffix(remaining, parts[1])
			return isMatch
		}
	}

	// Handle simple * wildcard for file extension patterns (*.go, *.sum, etc)
	if strings.HasPrefix(pattern, "*.") {
		ext := strings.TrimPrefix(pattern, "*")
		isMatch := strings.HasSuffix(path, ext)
		return isMatch
	}

	// Fall back to simple matching for simpler patterns
	matched, err := filepath.Match(pattern, path)
	if err != nil {
		slog.Error("Error matching pattern", "pattern", pattern, "path", path, "error", err)
		return false
	}

	return matched
}

// matchesPattern checks if a path matches the glob pattern
func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPattern) bool {
	patternInfo, err := pattern.AsPattern()
	if err != nil {
		slog.Error("Error parsing pattern", "pattern", pattern, "error", err)
		return false
	}

	basePath := patternInfo.GetBasePath()
	patternText := patternInfo.GetPattern()

	path = filepath.ToSlash(path)

	// For simple patterns without base path
	if basePath == "" {
		// Check if the pattern matches the full path or just the file extension
		fullPathMatch := matchesGlob(patternText, path)
		baseNameMatch := matchesGlob(patternText, filepath.Base(path))

		return fullPathMatch || baseNameMatch
	}

	// For relative patterns
	basePath = strings.TrimPrefix(basePath, "file://")
	basePath = filepath.ToSlash(basePath)

	// Make path relative to basePath for matching
	relPath, err := filepath.Rel(basePath, path)
	if err != nil {
		slog.Error("Error getting relative path", "path", path, "basePath", basePath, "error", err)
		return false
	}
	relPath = filepath.ToSlash(relPath)

	isMatch := matchesGlob(patternText, relPath)

	return isMatch
}

// debounceHandleFileEvent handles file events with debouncing to reduce notifications
func (w *WorkspaceWatcher) debounceHandleFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) {
	w.debounceMu.Lock()
	defer w.debounceMu.Unlock()

	// Create a unique key based on URI and change type
	key := fmt.Sprintf("%s:%d", uri, changeType)

	// Cancel existing timer if any
	if timer, exists := w.debounceMap[key]; exists {
		timer.Stop()
	}

	// Create new timer
	w.debounceMap[key] = time.AfterFunc(w.debounceTime, func() {
		w.handleFileEvent(ctx, uri, changeType)

		// Cleanup timer after execution
		w.debounceMu.Lock()
		delete(w.debounceMap, key)
		w.debounceMu.Unlock()
	})
}

// handleFileEvent sends file change notifications
func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) {
	// If the file is open and it's a change event, use didChange notification
	filePath := uri[7:] // Remove "file://" prefix

	if changeType == protocol.FileChangeType(protocol.Deleted) {
		// Always clear diagnostics for deleted files
		w.client.ClearDiagnosticsForURI(protocol.DocumentUri(uri))

		// If the file was open, close it in the LSP client
		if w.client.IsFileOpen(filePath) {
			if err := w.client.CloseFile(ctx, filePath); err != nil {
				slog.Debug("Error closing deleted file in LSP client", "file", filePath, "error", err)
				// Continue anyway - the file is gone
			}
		}
	} else if changeType == protocol.FileChangeType(protocol.Changed) {
		// For changed files, verify the file still exists before notifying
		if _, err := os.Stat(filePath); err != nil {
			if os.IsNotExist(err) {
				// File was deleted between the event and now - treat as delete
				slog.Debug("File deleted between change event and processing", "file", filePath)
				w.handleFileEvent(ctx, uri, protocol.FileChangeType(protocol.Deleted))
				return
			}
			slog.Error("Error getting file info", "path", filePath, "error", err)
			return
		}

		// File exists and is open, notify change
		if w.client.IsFileOpen(filePath) {
			err := w.client.NotifyChange(ctx, filePath)
			if err != nil {
				slog.Error("Error notifying change", "error", err)
			}
			return
		}
	} else if changeType == protocol.FileChangeType(protocol.Created) {
		// For created files, verify the file still exists before notifying
		if _, err := os.Stat(filePath); err != nil {
			if os.IsNotExist(err) {
				// File was deleted between the event and now - ignore
				slog.Debug("File deleted between create event and processing", "file", filePath)
				return
			}
			slog.Error("Error getting file info", "path", filePath, "error", err)
			return
		}
	}

	// Notify LSP server about the file event using didChangeWatchedFiles
	if err := w.notifyFileEvent(ctx, uri, changeType); err != nil {
		slog.Error("Error notifying LSP server about file event", "error", err)
	}
}

// notifyFileEvent sends a didChangeWatchedFiles notification for a file event
func (w *WorkspaceWatcher) notifyFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) error {
	cnf := config.Get()
	if cnf.DebugLSP {
		slog.Debug("Notifying file event",
			"uri", uri,
			"changeType", changeType,
		)
	}

	params := protocol.DidChangeWatchedFilesParams{
		Changes: []protocol.FileEvent{
			{
				URI:  protocol.DocumentUri(uri),
				Type: changeType,
			},
		},
	}

	return w.client.DidChangeWatchedFiles(ctx, params)
}

// getServerNameFromContext extracts the server name from the context
// This is a best-effort function that tries to identify which LSP server we're dealing with
func getServerNameFromContext(ctx context.Context) string {
	// First check if the server name is directly stored in the context
	if serverName, ok := ctx.Value("serverName").(string); ok && serverName != "" {
		return strings.ToLower(serverName)
	}

	// Otherwise, try to extract server name from the client command path
	if w, ok := ctx.Value("workspaceWatcher").(*WorkspaceWatcher); ok && w != nil && w.client != nil && w.client.Cmd != nil {
		path := strings.ToLower(w.client.Cmd.Path)

		// Extract server name from path
		if strings.Contains(path, "typescript") || strings.Contains(path, "tsserver") || strings.Contains(path, "vtsls") {
			return "typescript"
		} else if strings.Contains(path, "gopls") {
			return "gopls"
		} else if strings.Contains(path, "rust-analyzer") {
			return "rust-analyzer"
		} else if strings.Contains(path, "pyright") || strings.Contains(path, "pylsp") || strings.Contains(path, "python") {
			return "python"
		} else if strings.Contains(path, "clangd") {
			return "clangd"
		} else if strings.Contains(path, "jdtls") || strings.Contains(path, "java") {
			return "java"
		}

		// Return the base name as fallback
		return filepath.Base(path)
	}

	return "unknown"
}

// shouldPreloadFiles determines if we should preload files for a specific language server
// Some servers work better with preloaded files, others don't need it
func shouldPreloadFiles(serverName string) bool {
	// TypeScript/JavaScript servers typically need some files preloaded
	// to properly resolve imports and provide intellisense
	switch serverName {
	case "typescript", "typescript-language-server", "tsserver", "vtsls":
		return true
	case "java", "jdtls":
		// Java servers often need to see source files to build the project model
		return true
	default:
		// For most servers, we'll use lazy loading by default
		return false
	}
}

// Common patterns for directories and files to exclude
// TODO: make configurable
var (
	excludedDirNames = map[string]bool{
		".git":         true,
		"node_modules": true,
		"dist":         true,
		"build":        true,
		"out":          true,
		"bin":          true,
		".idea":        true,
		".vscode":      true,
		".cache":       true,
		"coverage":     true,
		"target":       true, // Rust build output
		"vendor":       true, // Go vendor directory
	}

	excludedFileExtensions = map[string]bool{
		".swp":   true,
		".swo":   true,
		".tmp":   true,
		".temp":  true,
		".bak":   true,
		".log":   true,
		".o":     true, // Object files
		".so":    true, // Shared libraries
		".dylib": true, // macOS shared libraries
		".dll":   true, // Windows shared libraries
		".a":     true, // Static libraries
		".exe":   true, // Windows executables
		".lock":  true, // Lock files
	}

	// Large binary files that shouldn't be opened
	largeBinaryExtensions = map[string]bool{
		".png":  true,
		".jpg":  true,
		".jpeg": true,
		".gif":  true,
		".bmp":  true,
		".ico":  true,
		".zip":  true,
		".tar":  true,
		".gz":   true,
		".rar":  true,
		".7z":   true,
		".pdf":  true,
		".mp3":  true,
		".mp4":  true,
		".mov":  true,
		".wav":  true,
		".wasm": true,
	}

	// Maximum file size to open (5MB)
	maxFileSize int64 = 5 * 1024 * 1024
)

// shouldExcludeDir returns true if the directory should be excluded from watching/opening
func shouldExcludeDir(dirPath string) bool {
	dirName := filepath.Base(dirPath)

	// Skip dot directories
	if strings.HasPrefix(dirName, ".") {
		return true
	}

	// Skip common excluded directories
	if excludedDirNames[dirName] {
		return true
	}

	return false
}

// shouldExcludeFile returns true if the file should be excluded from opening
func shouldExcludeFile(filePath string) bool {
	fileName := filepath.Base(filePath)
	cnf := config.Get()
	// Skip dot files
	if strings.HasPrefix(fileName, ".") {
		return true
	}

	// Check file extension
	ext := strings.ToLower(filepath.Ext(filePath))
	if excludedFileExtensions[ext] || largeBinaryExtensions[ext] {
		return true
	}

	// Skip temporary files
	if strings.HasSuffix(filePath, "~") {
		return true
	}

	// Skip numeric temporary files (often created by editors)
	if _, err := strconv.Atoi(fileName); err == nil {
		return true
	}

	// Check file size
	info, err := os.Stat(filePath)
	if err != nil {
		// If we can't stat the file, skip it
		return true
	}

	// Skip large files
	if info.Size() > maxFileSize {
		if cnf.DebugLSP {
			slog.Debug("Skipping large file",
				"path", filePath,
				"size", info.Size(),
				"maxSize", maxFileSize,
				"debug", cnf.Debug,
				"sizeMB", float64(info.Size())/(1024*1024),
				"maxSizeMB", float64(maxFileSize)/(1024*1024),
			)
		}
		return true
	}

	return false
}

// openMatchingFile opens a file if it matches any of the registered patterns
func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
	cnf := config.Get()
	// Skip directories and verify file exists
	info, err := os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			// File was deleted between event and processing - ignore
			slog.Debug("File deleted between event and openMatchingFile", "path", path)
			return
		}
		slog.Error("Error getting file info", "path", path, "error", err)
		return
	}

	if info.IsDir() {
		return
	}

	// Skip excluded files
	if shouldExcludeFile(path) {
		return
	}

	// Check if this path should be watched according to server registrations
	if watched, _ := w.isPathWatched(path); watched {
		// Get server name for specialized handling
		serverName := getServerNameFromContext(ctx)

		// Check if the file is a high-priority file that should be opened immediately
		// This helps with project initialization for certain language servers
		if isHighPriorityFile(path, serverName) {
			if cnf.DebugLSP {
				slog.Debug("Opening high-priority file", "path", path, "serverName", serverName)
			}
			if err := w.client.OpenFile(ctx, path); err != nil && cnf.DebugLSP {
				slog.Error("Error opening high-priority file", "path", path, "error", err)
			}
			return
		}

		// For non-high-priority files, we'll use different strategies based on server type
		if shouldPreloadFiles(serverName) {
			// For servers that benefit from preloading, open files but with limits

			// Check file size - for preloading we're more conservative
			if info.Size() > (1 * 1024 * 1024) { // 1MB limit for preloaded files
				if cnf.DebugLSP {
					slog.Debug("Skipping large file for preloading", "path", path, "size", info.Size())
				}
				return
			}

			// Check file extension for common source files
			ext := strings.ToLower(filepath.Ext(path))

			// Only preload source files for the specific language
			shouldOpen := false

			switch serverName {
			case "typescript", "typescript-language-server", "tsserver", "vtsls":
				shouldOpen = ext == ".ts" || ext == ".js" || ext == ".tsx" || ext == ".jsx"
			case "gopls":
				shouldOpen = ext == ".go"
			case "rust-analyzer":
				shouldOpen = ext == ".rs"
			case "python", "pyright", "pylsp":
				shouldOpen = ext == ".py"
			case "clangd":
				shouldOpen = ext == ".c" || ext == ".cpp" || ext == ".h" || ext == ".hpp"
			case "java", "jdtls":
				shouldOpen = ext == ".java"
			default:
				// For unknown servers, be conservative
				shouldOpen = false
			}

			if shouldOpen {
				// Don't need to check if it's already open - the client.OpenFile handles that
				if err := w.client.OpenFile(ctx, path); err != nil && cnf.DebugLSP {
					slog.Error("Error opening file", "path", path, "error", err)
				}
			}
		}
	}
}

// isHighPriorityFile determines if a file should be opened immediately
// regardless of the preloading strategy
func isHighPriorityFile(path string, serverName string) bool {
	fileName := filepath.Base(path)
	ext := filepath.Ext(path)

	switch serverName {
	case "typescript", "typescript-language-server", "tsserver", "vtsls":
		// For TypeScript, we want to open configuration files immediately
		return fileName == "tsconfig.json" ||
			fileName == "package.json" ||
			fileName == "jsconfig.json" ||
			// Also open main entry points
			fileName == "index.ts" ||
			fileName == "index.js" ||
			fileName == "main.ts" ||
			fileName == "main.js"
	case "gopls":
		// For Go, we want to open go.mod files immediately
		return fileName == "go.mod" ||
			fileName == "go.sum" ||
			// Also open main.go files
			fileName == "main.go"
	case "rust-analyzer":
		// For Rust, we want to open Cargo.toml files immediately
		return fileName == "Cargo.toml" ||
			fileName == "Cargo.lock" ||
			// Also open lib.rs and main.rs
			fileName == "lib.rs" ||
			fileName == "main.rs"
	case "python", "pyright", "pylsp":
		// For Python, open key project files
		return fileName == "pyproject.toml" ||
			fileName == "setup.py" ||
			fileName == "requirements.txt" ||
			fileName == "__init__.py" ||
			fileName == "__main__.py"
	case "clangd":
		// For C/C++, open key project files
		return fileName == "CMakeLists.txt" ||
			fileName == "Makefile" ||
			fileName == "compile_commands.json"
	case "java", "jdtls":
		// For Java, open key project files
		return fileName == "pom.xml" ||
			fileName == "build.gradle" ||
			ext == ".java" // Java servers often need to see source files
	}

	// For unknown servers, prioritize common configuration files
	return fileName == "package.json" ||
		fileName == "Makefile" ||
		fileName == "CMakeLists.txt" ||
		fileName == ".editorconfig"
}
