package indexer

import (
	"path"
	"sort"
	"strings"

	"github.com/zzet/gortex/internal/modules"
)

// workspaceMembershipIndex implements resolver.WorkspaceMembership: it
// maps a graph file path to the package-manager workspace member it
// belongs to.
//
// A monorepo is often a package-manager workspace — a root manifest
// (npm/yarn package.json, pnpm-workspace.yaml, Cargo.toml) that owns a
// set of member packages. Two member packages can each declare a
// package or symbol of the same name; when import resolution hits such
// a collision the resolver prefers the candidate whose file shares the
// importing file's member package. The identifier returned per file is
// the member package's repo-prefixed directory — files in the same
// member compare equal, files in different members compare unequal,
// and files outside every member directory return "".
//
// The index is built once at construction (one workspace detection +
// member-glob expansion per repo) and is read-only afterwards, so the
// lookup is allocation-free and safe for the resolver's parallel
// resolveEdge workers without locking.
type workspaceMembershipIndex struct {
	// memberDirs holds, per repo, the repo-relative directories that
	// are workspace members, sorted longest-first so the most-specific
	// enclosing member wins for a nested layout.
	memberDirs map[string][]string
}

// newWorkspaceMembershipIndex detects the package-manager workspace at
// each repo root and records its resolved member directories. Returns
// nil when no repo is a workspace root — callers treat a nil lookup as
// "no workspace signal", the pre-feature behaviour.
func newWorkspaceMembershipIndex(roots map[string]string) *workspaceMembershipIndex {
	memberDirs := map[string][]string{}
	for prefix, root := range roots {
		if root == "" {
			continue
		}
		manifest := modules.DetectWorkspace(root)
		if manifest == nil {
			continue
		}
		members := modules.ResolveWorkspaceMembers(root, manifest)
		if len(members) == 0 {
			continue
		}
		dirs := make([]string, 0, len(members))
		for _, m := range members {
			dirs = append(dirs, m.Dir)
		}
		// Longest-first: a member nested inside another member dir
		// must be matched before its ancestor.
		sort.Slice(dirs, func(i, j int) bool {
			return len(dirs[i]) > len(dirs[j])
		})
		memberDirs[prefix] = dirs
	}
	if len(memberDirs) == 0 {
		return nil
	}
	return &workspaceMembershipIndex{memberDirs: memberDirs}
}

// Lookup is the resolver.WorkspaceMembership entry point. filePath is a
// graph file path — repo-prefixed in multi-repo mode, bare in
// single-repo mode. It returns the repo-prefixed directory of the
// workspace member that owns the file, or "" when the file belongs to
// no workspace member.
func (x *workspaceMembershipIndex) Lookup(filePath string) string {
	if x == nil || filePath == "" {
		return ""
	}
	filePath = path.Clean(filePath)
	// Try every repo: an entry with an empty prefix is single-repo
	// mode (graph paths carry no prefix); a non-empty prefix must be a
	// path ancestor of the file.
	for prefix, dirs := range x.memberDirs {
		rel := filePath
		if prefix != "" {
			if filePath != prefix && !strings.HasPrefix(filePath, prefix+"/") {
				continue
			}
			rel = strings.TrimPrefix(filePath, prefix+"/")
		}
		for _, dir := range dirs {
			if rel == dir || strings.HasPrefix(rel, dir+"/") {
				// Identifier is the repo-prefixed member dir so two
				// repos with an identically-named member directory do
				// not collide.
				if prefix == "" {
					return dir
				}
				return prefix + "/" + dir
			}
		}
	}
	return ""
}

// indexerWorkspaceMembership lazily builds this Indexer's workspace
// membership index and resolves a file path through it. Installed on
// the resolver via SetWorkspaceMembership. Lazy because the repo root
// and prefix are not final until after New().
func (idx *Indexer) indexerWorkspaceMembership(filePath string) string {
	idx.workspaceMembersOnce.Do(func() {
		idx.workspaceMembers = newWorkspaceMembershipIndex(
			map[string]string{idx.repoPrefix: idx.rootPath})
	})
	return idx.workspaceMembers.Lookup(filePath)
}
