package packagemanager

import (
	"path/filepath"

	"github.com/vercel/turbo/cli/internal/doublestar"
	"github.com/vercel/turbo/cli/internal/turbopath"
)

// PackageType represents the mode in which turbo is running.
type PackageType string

const (
	// Single is for single-package mode.
	Single PackageType = "single"
	// Multi is for monorepo mode.
	Multi PackageType = "multi"
)

func candidateDirectoryWorkspaceGlobs(directory turbopath.AbsoluteSystemPath) []string {
	packageManagers := []PackageManager{
		nodejsNpm,
		nodejsPnpm,
	}

	for _, pm := range packageManagers {
		globs, err := pm.getWorkspaceGlobs(directory)
		if err != nil {
			// Try the other package manager workspace formats.
			continue
		}

		return globs
	}

	return nil
}

func isOneOfTheWorkspaces(globs []string, nearestPackageJSONDir turbopath.AbsoluteSystemPath, currentPackageJSONDir turbopath.AbsoluteSystemPath) bool {
	for _, glob := range globs {
		globpattern := currentPackageJSONDir.UntypedJoin(filepath.FromSlash(glob)).ToString()
		match, _ := doublestar.PathMatch(globpattern, nearestPackageJSONDir.ToString())
		if match {
			return true
		}
	}

	return false
}

// InferRoot identifies which directory we should treat as the root, and which mode
// turbo should be in when operating at that directory.
func InferRoot(directory turbopath.AbsoluteSystemPath) (turbopath.AbsoluteSystemPath, PackageType) {
	// Go doesn't have iterators, so this is very not-elegant.

	// Scenarios:
	// 0. Has a turbo.json but doesn't have a peer package.json. directory + multi
	// 1. Nearest turbo.json, check peer package.json/pnpm-workspace.yaml.
	//    A. Has workspaces, multi package mode.
	//    B. No workspaces, single package mode.
	// 2. If no turbo.json find the closest package.json parent.
	//    A. No parent package.json, default to current behavior.
	//    B. Nearest package.json defines workspaces. Can't be in single-package mode, so we bail. (This could be changed in the future.)
	// 3. Closest package.json does not define workspaces. Traverse toward the root looking for package.jsons.
	//    A. No parent package.json with workspaces. nearestPackageJson + single
	//    B. Stop at the first one that has workspaces.
	//       i. If we are one of the workspaces, directory + multi. (This could be changed in the future.)
	//       ii. If we're not one of the workspaces, nearestPackageJson + single.

	nearestTurboJSON, findTurboJSONErr := directory.Findup("turbo.json")
	if nearestTurboJSON == "" || findTurboJSONErr != nil {
		// We didn't find a turbo.json. We're in situation 2 or 3.

		// Unroll the first loop for Scenario 2
		nearestPackageJSON, nearestPackageJSONErr := directory.Findup("package.json")

		// If we fail to find any package.json files we aren't in single package mode.
		// We let things go through our existing failure paths.
		// Scenario 2A.
		if nearestPackageJSON == "" || nearestPackageJSONErr != nil {
			return directory, Multi
		}

		// If we find a package.json which has workspaces we aren't in single package mode.
		// We let things go through our existing failure paths.
		// Scenario 2B.
		if candidateDirectoryWorkspaceGlobs(nearestPackageJSON.Dir()) != nil {
			// In a future world we could maybe change this behavior.
			// return nearestPackageJson.Dir(), Multi
			return directory, Multi
		}

		// Scenario 3.
		// Find the nearest package.json that has workspaces.
		// If found _and_ the nearestPackageJson is one of the workspaces, thatPackageJson + multi.
		// Else, nearestPackageJson + single
		cursor := nearestPackageJSON.Dir().UntypedJoin("..")
		for {
			nextPackageJSON, nextPackageJSONErr := cursor.Findup("package.json")
			if nextPackageJSON == "" || nextPackageJSONErr != nil {
				// We haven't found a parent defining workspaces.
				// So we're single package mode at nearestPackageJson.
				// Scenario 3A.
				return nearestPackageJSON.Dir(), Single
			}

			// Found a package.json file, see if it has workspaces.
			// Workspaces are not allowed to be recursive, so we know what to
			// return the moment we find something with workspaces.
			globs := candidateDirectoryWorkspaceGlobs(nextPackageJSON.Dir())
			if globs != nil {
				if isOneOfTheWorkspaces(globs, nearestPackageJSON.Dir(), nextPackageJSON.Dir()) {
					// If it has workspaces, and nearestPackageJson is one of them, we're multi.
					// We don't infer in this scenario.
					// Scenario 3BI.
					// TODO: return nextPackageJson.Dir(), Multi
					return directory, Multi
				}

				// We found a parent with workspaces, but we're not one of them.
				// We choose to operate in single package mode.
				// Scenario 3BII
				return nearestPackageJSON.Dir(), Single
			}

			// Loop around and see if we have another parent.
			cursor = nextPackageJSON.Dir().UntypedJoin("..")
		}
	} else {
		// If there is no sibling package.json we do no inference.
		siblingPackageJSONPath := nearestTurboJSON.Dir().UntypedJoin("package.json")
		if !siblingPackageJSONPath.Exists() {
			// We do no inference.
			// Scenario 0
			return directory, Multi
		}

		if candidateDirectoryWorkspaceGlobs(nearestTurboJSON.Dir()) != nil {
			// Scenario 1A.
			return nearestTurboJSON.Dir(), Multi
		}

		// Scenario 1B.
		return nearestTurboJSON.Dir(), Single
	}
}
