package modload

import (
	"context"
	"errors"
	"fmt"
	"io/fs"
	"maps"
	"path"
	"path/filepath"
	"runtime"
	"slices"
	"strings"
	"sync/atomic"

	"cuelang.org/go/cue/ast"
	"cuelang.org/go/internal/mod/modpkgload"
	"cuelang.org/go/internal/mod/modrequirements"
	"cuelang.org/go/internal/mod/semver"
	"cuelang.org/go/internal/par"
	"cuelang.org/go/mod/modfile"
	"cuelang.org/go/mod/modregistry"
	"cuelang.org/go/mod/module"
)

// UpdateVersions returns the main module's module file with the specified module versions
// updated if possible and added if not already present. It returns an error if asked
// to downgrade a module below a version already required by an external dependency.
//
// A module in the versions slice can be specified as one of the following:
//   - $module@$fullVersion: a specific exact version
//   - $module@$partialVersion: a non-canonical version
//     specifies the latest version that has the same major/minor numbers.
//   - $module@latest: the latest non-prerelease version, or latest prerelease version if
//     there is no non-prerelease version
//   - $module: equivalent to $module@latest if $module doesn't have a default major
//     version or $module@$majorVersion if it does, where $majorVersion is the
//     default major version for $module.
func UpdateVersions(ctx context.Context, fsys fs.FS, modRoot string, reg Registry, versions []string) (*modfile.File, error) {
	mf, err := readPublishedModuleFile(fsys, modRoot)
	if err != nil {
		return nil, err
	}
	mainModuleVersion, err := module.NewVersion(mf.QualifiedModule(), "")
	if err != nil {
		return nil, fmt.Errorf("cue.mod/module.cue: invalid module path: %v", err)
	}
	rs := modrequirements.NewRequirements(mf.QualifiedModule(), reg, mf.DepVersions(), mf.DefaultMajorVersions())
	mversions, err := resolveUpdateVersions(ctx, reg, rs, mainModuleVersion, versions)
	if err != nil {
		return nil, err
	}
	// Now we know what versions we want to update to, make a new set of
	// requirements with these versions in place.

	mversionsMap := make(map[string]module.Version)
	for _, v := range mversions {
		// Check existing membership of the map: if the same module has been specified
		// twice, then choose t
		if v1, ok := mversionsMap[v.Path()]; ok && v1.Version() != v.Version() {
			// The same module has been specified twice with different requirements.
			// Treat it as an error (an alternative approach might be to choose the greater
			// version, but making it an error seems more appropriate to the "choose exact
			// version" semantics of UpdateVersions.
			return nil, fmt.Errorf("conflicting version update requirements %v vs %v", v1, v)
		}
		mversionsMap[v.Path()] = v
	}
	g, err := rs.Graph(ctx)
	if err != nil {
		return nil, fmt.Errorf("cannot determine module graph: %v", err)
	}
	var newVersions []module.Version
	for _, v := range g.BuildList() {
		if v.Path() == mainModuleVersion.Path() {
			continue
		}
		if newv, ok := mversionsMap[v.Path()]; ok {
			newVersions = append(newVersions, newv)
			delete(mversionsMap, v.Path())
		} else {
			newVersions = append(newVersions, v)
		}
	}
	newVersions = slices.AppendSeq(newVersions, maps.Values(mversionsMap))
	slices.SortFunc(newVersions, module.Version.Compare)
	rs = modrequirements.NewRequirements(mf.QualifiedModule(), reg, newVersions, mf.DefaultMajorVersions())
	g, err = rs.Graph(ctx)
	if err != nil {
		return nil, fmt.Errorf("cannot determine new module graph: %v", err)
	}
	// Now check that the resulting versions are the ones we wanted.
	for _, v := range mversions {
		actualVers := g.Selected(v.Path())
		if actualVers != v.Version() {
			return nil, fmt.Errorf("other requirements prevent changing module %v to version %v (actual selected version: %v)", v.Path(), v.Version(), actualVers)
		}
	}
	// Make a new requirements with the selected versions of the above as roots.
	var finalVersions []module.Version
	for _, v := range g.BuildList() {
		if v.Path() != mainModuleVersion.Path() {
			finalVersions = append(finalVersions, v)
		}
	}
	rs = modrequirements.NewRequirements(mf.QualifiedModule(), reg, finalVersions, mf.DefaultMajorVersions())
	return modfileFromRequirements(mf, rs, nil, rs.DefaultMajorVersions(), nil), nil
}

// ResolveAbsolutePackage resolves a package in a standalone fashion, irrespective
// of a module file. It returns the module containing that package and the location of the package.
//
// It tries to avoid hitting the network unless necessary by using cached results where available.
func ResolveAbsolutePackage(ctx context.Context, reg Registry, p string) (module.Version, module.SourceLoc, error) {
	fail := func(err error) (module.Version, module.SourceLoc, error) {
		return module.Version{}, module.SourceLoc{}, err
	}
	failf := func(format string, args ...interface{}) (module.Version, module.SourceLoc, error) {
		return fail(fmt.Errorf(format, args...))
	}
	if filepath.IsAbs(p) || path.IsAbs(p) {
		return failf("%q is not a package path", p)
	}
	ip := ast.ParseImportPath(p)
	// Before any further lookup, check that the path without the version specifier is valid;
	// for example foo.com/bar/@latest would be an example of an invalid path.
	ip1 := ip
	ip1.Version = ""
	if err := module.CheckImportPath(ip1.String()); err != nil {
		return fail(err)
	}

	tryResolve := func(fetch func(m module.Version) (module.SourceLoc, error)) (module.Version, module.SourceLoc, error) {
		locs, err := modpkgload.FindPackageLocations(ctx, p, func(ctx context.Context, prefixPath string) (module.Version, error) {
			mv, err := resolveModuleVersion(ctx, reg, nil, prefixPath+"@"+ip.Version)
			if errors.Is(err, errNoVersionsFound) {
				return module.Version{}, nil
			}
			return mv, err
		}, func(ctx context.Context, m module.Version) (loc module.SourceLoc, isLocal bool, err error) {
			loc, err = fetch(m)
			if errors.Is(err, modregistry.ErrNotFound) {
				err = nil
			}
			return loc, false, err
		})
		if err != nil {
			return fail(err)
		}
		if len(locs) == 1 {
			// We've got exactly one cache hit. Use it.
			return locs[0].Module, locs[0].Locs[0], nil
		}
		if len(locs) > 1 {
			return fail(&modpkgload.AmbiguousImportError{ImportPath: p, Locations: locs})
		}
		return fail(&modpkgload.ImportMissingError{Path: p})
	}

	if reg, ok := reg.(modpkgload.CachedRegistry); ok && ip.Version != "" && semver.Canonical(ip.Version) == ip.Version {
		// It's a canonical version and we're using a caching registry implementation.
		// We might be able to avoid hitting the network.
		mv, loc, err := tryResolve(reg.FetchFromCache)
		if err == nil || !errors.As(err, new(*modpkgload.ImportMissingError)) {
			return mv, loc, err
		}
		// Not found in cache. Try again with the non-cached version.
	}
	return tryResolve(func(m module.Version) (module.SourceLoc, error) {
		return reg.Fetch(ctx, m)
	})
}

var errNoVersionsFound = fmt.Errorf("no versions found")

// resolveModuleVersion resolves a module/version query to a canonical module version.
//
// The version may take any of the following forms:
//
//	$module@v1.2.3	- absolute version.
//	$module			- latest version
//	$module@v1		- latest version at v1
//	$module@v1.2	- latest version within v1.1
//	$module@latest	- same as $module
//	$module@v1.latest	- same as @v1
//
// If rs is non-nil, it will be used to choose a default major version when no
// major version is specified.
//
// It returns an errNoVersionsFound error if there are no versions for the query but
// all else is OK.
//
// TODO could support queries like <=v1.2.3 etc
func resolveModuleVersion(ctx context.Context, reg Registry, rs *modrequirements.Requirements, v string) (module.Version, error) {
	if mv, err := module.ParseVersion(v); err == nil {
		// It's already a canonical version; nothing to do.
		return mv, nil
	}
	mpath, vers, ok := strings.Cut(v, "@")
	if !ok {
		if rs != nil {
			if major, status := rs.DefaultMajorVersion(mpath); status == modrequirements.ExplicitDefault {
				// TODO allow a non-explicit default too?
				vers = major
			}
		}
		if vers == "" {
			vers = "latest"
		}
	}

	if err := module.CheckPathWithoutVersion(mpath); err != nil {
		return module.Version{}, fmt.Errorf("%w: invalid module path in %q", errNoVersionsFound, v)
	}
	versionPrefix := ""
	switch {
	case vers == "latest":
	case strings.HasSuffix(vers, ".latest"):
		versionPrefix = strings.TrimSuffix(vers, ".latest")
		if !semver.IsValid(versionPrefix) {
			return module.Version{}, fmt.Errorf("invalid version specified %q", vers)
		}
		if semver.Canonical(versionPrefix) == versionPrefix {
			// TODO maybe relax this a bit to allow v1.2.3.latest ?
			return module.Version{}, fmt.Errorf("cannot use .latest on canonical version %q", vers)
		}
	default:
		if !semver.IsValid(vers) {
			return module.Version{}, fmt.Errorf("%q does not specify a valid semantic version", v)
		}
		if semver.Build(vers) != "" {
			return module.Version{}, fmt.Errorf("build version suffixes not supported (%v)", v)
		}
		// It's a valid version but has no build suffix and it's not canonical,
		// which means it must be either a major-only or major-minor, so
		// the conforming canonical versions must have it as a prefix, with
		// a dot separating the last component and the next.
		versionPrefix = vers + "."
	}
	allVersions, err := reg.ModuleVersions(ctx, mpath)
	if err != nil {
		return module.Version{}, err
	}
	possibleVersions := make([]string, 0, len(allVersions))
	for _, v := range allVersions {
		if strings.HasPrefix(v, versionPrefix) {
			possibleVersions = append(possibleVersions, v)
		}
	}
	if len(possibleVersions) == 0 {
		return module.Version{}, fmt.Errorf("%w for module %v", errNoVersionsFound, v)
	}
	chosen := LatestVersion(possibleVersions)
	mv, err := module.NewVersion(mpath, chosen)
	if err != nil {
		// Should never happen, because we've checked that
		// mpath is valid and ModuleVersions
		// should always return valid semver versions.
		return module.Version{}, err
	}
	return mv, nil
}

// resolveUpdateVersions resolves a set of version strings as accepted by [UpdateVersions]
// into the actual module versions they represent.
func resolveUpdateVersions(ctx context.Context, reg Registry, rs *modrequirements.Requirements, mainModuleVersion module.Version, versions []string) ([]module.Version, error) {
	work := par.NewQueue(runtime.GOMAXPROCS(0))
	mversions := make([]module.Version, len(versions))
	var queryErr atomic.Pointer[error]
	setError := func(err error) {
		queryErr.CompareAndSwap(nil, &err)
	}
	for i, v := range versions {
		if mv, err := module.ParseVersion(v); err == nil {
			// It's already canonical: nothing more to do.
			mversions[i] = mv
			continue
		}
		work.Add(func() {
			mv, err := resolveModuleVersion(ctx, reg, rs, v)
			if err != nil {
				setError(err)
			} else {
				mversions[i] = mv
			}
		})
	}
	<-work.Idle()
	if errPtr := queryErr.Load(); errPtr != nil {
		return nil, *errPtr
	}
	for _, v := range mversions {
		if v.Path() == mainModuleVersion.Path() {
			return nil, fmt.Errorf("cannot update version of main module")
		}
	}
	return mversions, nil
}
