package project

import (
	"fmt"
	"maps"
	"slices"
	"strings"
	"sync"
	"sync/atomic"

	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/module"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
)

const (
	minWatchLocationDepth = 2
)

type fileSystemWatcherKey struct {
	pattern string
	kind    lsproto.WatchKind
}

type fileSystemWatcherValue struct {
	count int
	id    WatcherID
}

type patternsAndIgnored struct {
	patterns []string
	ignored  map[string]struct{}
}

func toFileSystemWatcherKey(w *lsproto.FileSystemWatcher) fileSystemWatcherKey {
	if w.GlobPattern.RelativePattern != nil {
		panic("relative globs not implemented")
	}
	kind := w.Kind
	if kind == nil {
		kind = ptrTo(lsproto.WatchKindCreate | lsproto.WatchKindChange | lsproto.WatchKindDelete)
	}
	return fileSystemWatcherKey{pattern: *w.GlobPattern.Pattern, kind: *kind}
}

type WatcherID string

var watcherID atomic.Uint64

type WatchedFiles[T any] struct {
	name                string
	watchKind           lsproto.WatchKind
	computeGlobPatterns func(input T) patternsAndIgnored

	mu                  sync.RWMutex
	input               T
	computeWatchersOnce sync.Once
	watchers            []*lsproto.FileSystemWatcher
	ignored             map[string]struct{}
	id                  uint64
}

func NewWatchedFiles[T any](name string, watchKind lsproto.WatchKind, computeGlobPatterns func(input T) patternsAndIgnored) *WatchedFiles[T] {
	return &WatchedFiles[T]{
		id:                  watcherID.Add(1),
		name:                name,
		watchKind:           watchKind,
		computeGlobPatterns: computeGlobPatterns,
	}
}

func (w *WatchedFiles[T]) Watchers() (WatcherID, []*lsproto.FileSystemWatcher, map[string]struct{}) {
	w.computeWatchersOnce.Do(func() {
		w.mu.Lock()
		defer w.mu.Unlock()
		result := w.computeGlobPatterns(w.input)
		globs := result.patterns
		ignored := result.ignored
		// ignored is only used for logging and doesn't affect watcher identity
		w.ignored = ignored
		if !slices.EqualFunc(w.watchers, globs, func(a *lsproto.FileSystemWatcher, b string) bool {
			return *a.GlobPattern.Pattern == b
		}) {
			w.watchers = core.Map(globs, func(glob string) *lsproto.FileSystemWatcher {
				return &lsproto.FileSystemWatcher{
					GlobPattern: lsproto.PatternOrRelativePattern{
						Pattern: &glob,
					},
					Kind: &w.watchKind,
				}
			})
			w.id = watcherID.Add(1)
		}
	})

	w.mu.RLock()
	defer w.mu.RUnlock()
	return WatcherID(fmt.Sprintf("%s watcher %d", w.name, w.id)), w.watchers, w.ignored
}

func (w *WatchedFiles[T]) ID() WatcherID {
	if w == nil {
		return ""
	}
	id, _, _ := w.Watchers()
	return id
}

func (w *WatchedFiles[T]) Name() string {
	return w.name
}

func (w *WatchedFiles[T]) WatchKind() lsproto.WatchKind {
	return w.watchKind
}

func (w *WatchedFiles[T]) Clone(input T) *WatchedFiles[T] {
	w.mu.RLock()
	defer w.mu.RUnlock()
	return &WatchedFiles[T]{
		name:                w.name,
		watchKind:           w.watchKind,
		computeGlobPatterns: w.computeGlobPatterns,
		watchers:            w.watchers,
		input:               input,
	}
}

func createResolutionLookupGlobMapper(workspaceDirectory string, libDirectory string, currentDirectory string, useCaseSensitiveFileNames bool) func(data map[tspath.Path]string) patternsAndIgnored {
	comparePathsOptions := tspath.ComparePathsOptions{
		CurrentDirectory:          currentDirectory,
		UseCaseSensitiveFileNames: useCaseSensitiveFileNames,
	}

	return func(data map[tspath.Path]string) patternsAndIgnored {
		var ignored map[string]struct{}
		var seenDirs collections.Set[string]
		var includeWorkspace, includeRoot, includeLib bool
		var nodeModulesDirectories, externalDirectories map[tspath.Path]string

		for path, fileName := range data {
			// Assuming all of the input paths are filenames, we can avoid
			// duplicate work by only taking one file per dir, since their outputs
			// will always be the same.
			if !seenDirs.AddIfAbsent(tspath.GetDirectoryPath(string(path))) {
				continue
			}

			if tspath.ContainsPath(workspaceDirectory, fileName, comparePathsOptions) {
				includeWorkspace = true
			} else if tspath.ContainsPath(currentDirectory, fileName, comparePathsOptions) {
				includeRoot = true
			} else if tspath.ContainsPath(libDirectory, fileName, comparePathsOptions) {
				includeLib = true
			} else if idx := strings.Index(fileName, "/node_modules/"); idx != -1 {
				if nodeModulesDirectories == nil {
					nodeModulesDirectories = make(map[tspath.Path]string)
				}
				dir := fileName[:idx+len("/node_modules")]
				nodeModulesDirectories[tspath.ToPath(dir, currentDirectory, useCaseSensitiveFileNames)] = dir
			} else {
				if externalDirectories == nil {
					externalDirectories = make(map[tspath.Path]string)
				}
				externalDirectories[path.GetDirectoryPath()] = tspath.GetDirectoryPath(fileName)
			}
		}

		var globs []string
		if includeWorkspace {
			globs = append(globs, getRecursiveGlobPattern(workspaceDirectory))
		}
		if includeRoot {
			globs = append(globs, getRecursiveGlobPattern(currentDirectory))
		}
		if includeLib {
			globs = append(globs, getRecursiveGlobPattern(libDirectory))
		}
		for _, dir := range nodeModulesDirectories {
			globs = append(globs, getRecursiveGlobPattern(dir))
		}
		if len(externalDirectories) > 0 {
			externalDirectoryParents, ignoredExternalDirs := tspath.GetCommonParents(
				slices.Collect(maps.Values(externalDirectories)),
				minWatchLocationDepth,
				getPathComponentsForWatching,
				comparePathsOptions,
			)
			slices.Sort(externalDirectoryParents)
			ignored = ignoredExternalDirs
			for _, dir := range externalDirectoryParents {
				globs = append(globs, getRecursiveGlobPattern(dir))
			}
		}

		return patternsAndIgnored{
			patterns: globs,
			ignored:  ignored,
		}
	}
}

func getTypingsLocationsGlobs(
	typingsFiles []string,
	typingsLocation string,
	workspaceDirectory string,
	currentDirectory string,
	useCaseSensitiveFileNames bool,
) patternsAndIgnored {
	var includeTypingsLocation, includeWorkspace bool
	externalDirectories := make(map[tspath.Path]string)
	globs := make(map[tspath.Path]string)
	comparePathsOptions := tspath.ComparePathsOptions{
		CurrentDirectory:          currentDirectory,
		UseCaseSensitiveFileNames: useCaseSensitiveFileNames,
	}
	for _, file := range typingsFiles {
		if tspath.ContainsPath(typingsLocation, file, comparePathsOptions) {
			includeTypingsLocation = true
		} else if !tspath.ContainsPath(workspaceDirectory, file, comparePathsOptions) {
			directory := tspath.GetDirectoryPath(file)
			externalDirectories[tspath.ToPath(directory, currentDirectory, useCaseSensitiveFileNames)] = directory
		} else {
			includeWorkspace = true
		}
	}
	externalDirectoryParents, ignored := tspath.GetCommonParents(
		slices.Collect(maps.Values(externalDirectories)),
		minWatchLocationDepth,
		getPathComponentsForWatching,
		comparePathsOptions,
	)
	slices.Sort(externalDirectoryParents)
	if includeWorkspace {
		globs[tspath.ToPath(workspaceDirectory, currentDirectory, useCaseSensitiveFileNames)] = getRecursiveGlobPattern(workspaceDirectory)
	}
	if includeTypingsLocation {
		globs[tspath.ToPath(typingsLocation, currentDirectory, useCaseSensitiveFileNames)] = getRecursiveGlobPattern(typingsLocation)
	}
	for _, dir := range externalDirectoryParents {
		globs[tspath.ToPath(dir, currentDirectory, useCaseSensitiveFileNames)] = getRecursiveGlobPattern(dir)
	}
	return patternsAndIgnored{
		patterns: slices.Collect(maps.Values(globs)),
		ignored:  ignored,
	}
}

func getPathComponentsForWatching(path string, currentDirectory string) []string {
	components := tspath.GetPathComponents(path, currentDirectory)
	rootLength := perceivedOsRootLengthForWatching(components)
	if rootLength <= 1 {
		return components
	}
	newRoot := tspath.CombinePaths(components[0], components[1:rootLength]...)
	return append([]string{newRoot}, components[rootLength:]...)
}

func perceivedOsRootLengthForWatching(pathComponents []string) int {
	length := len(pathComponents)
	if length <= 1 {
		return length
	}
	if strings.HasPrefix(pathComponents[0], "//") {
		// Group UNC roots (//server/share) into a single component
		return 2
	}
	if len(pathComponents[0]) == 3 && tspath.IsVolumeCharacter(pathComponents[0][0]) && pathComponents[0][1] == ':' && pathComponents[0][2] == '/' {
		// Windows-style volume
		if strings.EqualFold(pathComponents[1], "users") {
			// Group C:/Users/username into a single component
			return min(3, length)
		}
		return 1
	}
	if pathComponents[1] == "home" {
		// Group /home/username into a single component
		return min(3, length)
	}
	return 1
}

func ptrTo[T any](v T) *T {
	return &v
}

type resolutionWithLookupLocations interface {
	GetLookupLocations() *module.LookupLocations
}

func extractLookups[T resolutionWithLookupLocations](
	projectToPath func(string) tspath.Path,
	failedLookups map[tspath.Path]string,
	affectingLocations map[tspath.Path]string,
	cache map[tspath.Path]module.ModeAwareCache[T],
) {
	for _, resolvedModulesInFile := range cache {
		for _, resolvedModule := range resolvedModulesInFile {
			for _, failedLookupLocation := range resolvedModule.GetLookupLocations().FailedLookupLocations {
				path := projectToPath(failedLookupLocation)
				if _, ok := failedLookups[path]; !ok {
					failedLookups[path] = failedLookupLocation
				}
			}
			for _, affectingLocation := range resolvedModule.GetLookupLocations().AffectingLocations {
				path := projectToPath(affectingLocation)
				if _, ok := affectingLocations[path]; !ok {
					affectingLocations[path] = affectingLocation
				}
			}
		}
	}
}

func getNonRootFileGlobs(workspaceDir string, libDirectory string, sourceFiles []*ast.SourceFile, rootFiles map[tspath.Path]string, comparePathsOptions tspath.ComparePathsOptions) patternsAndIgnored {
	var globs []string
	var includeWorkspace, includeLib bool
	var ignored map[string]struct{}
	externalDirectories := make([]string, 0, max(0, len(sourceFiles)-len(rootFiles)))
	for _, sourceFile := range sourceFiles {
		if _, ok := rootFiles[sourceFile.Path()]; !ok {
			if tspath.ContainsPath(workspaceDir, sourceFile.FileName(), comparePathsOptions) {
				includeWorkspace = true
			} else if tspath.ContainsPath(libDirectory, sourceFile.FileName(), comparePathsOptions) {
				includeLib = true
			} else {
				externalDirectories = append(externalDirectories, tspath.GetDirectoryPath(sourceFile.FileName()))
			}
		}
	}

	if includeWorkspace {
		globs = append(globs, getRecursiveGlobPattern(workspaceDir))
	}
	if includeLib {
		globs = append(globs, getRecursiveGlobPattern(libDirectory))
	}
	if len(externalDirectories) > 0 {
		commonParents, ignoredDirs := tspath.GetCommonParents(
			externalDirectories,
			minWatchLocationDepth,
			getPathComponentsForWatching,
			comparePathsOptions,
		)
		globs = append(globs, core.Map(commonParents, func(dir string) string {
			return getRecursiveGlobPattern(dir)
		})...)
		ignored = ignoredDirs
	}
	return patternsAndIgnored{
		patterns: globs,
		ignored:  ignored,
	}
}

func getRecursiveGlobPattern(directory string) string {
	return fmt.Sprintf("%s/%s", tspath.RemoveTrailingDirectorySeparator(directory), "**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json}")
}
