package mod

import (
	"context"
	"fmt"
	"os"
	"path"
	"strings"

	"github.com/gofrs/flock"
	"github.com/rs/zerolog/log"
	"go.dagger.io/dagger/pkg"
)

const (
	UniverseVersionConstraint = ">= 0.1.0, < 0.2"
)

func isUniverse(repoName string) bool {
	return strings.HasPrefix(strings.ToLower(repoName), pkg.UniverseModule)
}

// IsUniverseLatest check that current universe is up-to-date or no
// It returns true if universe is up-to-date, otherwise false
// If universe was not installed from `dagger mod get`, it will
// not compare anything.
//
// The latest tag is fetch from universe repo itself.
func IsUniverseLatest(ctx context.Context, workspace string) (bool, error) {
	modfile, err := readPath(workspace)
	if err != nil {
		return false, err
	}

	req, err := newRequire(pkg.UniverseModule, UniverseVersionConstraint)
	if err != nil {
		return false, err
	}

	// Get current universe version
	universe := modfile.searchInstalledRequire(req)
	if universe == nil {
		return false, fmt.Errorf("universe not installed")
	}

	tmpPath := path.Join(modfile.workspacePath, tmpBasePath, req.fullPath())
	defer os.RemoveAll(tmpPath)

	repo, err := clone(ctx, req, tmpPath, "", "")
	if err != nil {
		return false, err
	}

	// Get latest tag
	latestTag, err := repo.latestTag(ctx, req.versionConstraint)
	if err != nil {
		return false, err
	}

	c, err := compareVersions(latestTag, universe.version)
	if err != nil {
		return false, err
	}
	return !(c == 1), nil
}

func Install(ctx context.Context, workspace, repoName, versionConstraint string) (*Require, error) {
	lg := log.Ctx(ctx)

	if isUniverse(repoName) {
		// override versionConstraint to lock the version of universe we vendor
		versionConstraint = UniverseVersionConstraint
	}

	lg.Debug().Str("name", repoName).Msg("installing module")
	require, err := newRequire(repoName, versionConstraint)
	if err != nil {
		return nil, err
	}

	modfile, err := readPath(workspace)
	if err != nil {
		return nil, err
	}

	fileLockPath := path.Join(workspace, lockFilePath)
	fileLock := flock.New(fileLockPath)
	if err := fileLock.Lock(); err != nil {
		return nil, err
	}

	defer func() {
		fileLock.Unlock()
		os.Remove(fileLockPath)
	}()

	err = modfile.install(ctx, require)
	if err != nil {
		return nil, err
	}

	if err = modfile.write(); err != nil {
		return nil, err
	}

	return require, nil
}

func InstallAll(ctx context.Context, workspace string, repoNames []string) ([]*Require, error) {
	installedRequires := make([]*Require, 0, len(repoNames))
	var err error

	for _, repoName := range repoNames {
		var require *Require

		if require, err = Install(ctx, workspace, repoName, ""); err != nil {
			continue
		}

		installedRequires = append(installedRequires, require)
	}

	return installedRequires, err
}

func Update(ctx context.Context, workspace, repoName, versionConstraint string) (*Require, error) {
	lg := log.Ctx(ctx)

	if isUniverse(repoName) {
		// override versionConstraint to lock the version of universe we vendor
		versionConstraint = UniverseVersionConstraint
	}

	lg.Debug().Str("name", repoName).Msg("updating module")
	require, err := newRequire(repoName, versionConstraint)
	if err != nil {
		return nil, err
	}

	modfile, err := readPath(workspace)
	if err != nil {
		return nil, err
	}

	fileLockPath := path.Join(workspace, lockFilePath)
	fileLock := flock.New(fileLockPath)
	if err := fileLock.Lock(); err != nil {
		return nil, err
	}

	defer func() {
		fileLock.Unlock()
		os.Remove(fileLockPath)
	}()

	updatedRequire, err := modfile.updateToLatest(ctx, require)
	if err != nil {
		return nil, err
	}

	if err = modfile.write(); err != nil {
		return nil, err
	}

	return updatedRequire, nil
}

func UpdateAll(ctx context.Context, workspace string, repoNames []string) ([]*Require, error) {
	updatedRequires := make([]*Require, 0, len(repoNames))
	var err error

	for _, repoName := range repoNames {
		var require *Require

		if require, err = Update(ctx, workspace, repoName, ""); err != nil {
			continue
		}

		updatedRequires = append(updatedRequires, require)
	}

	return updatedRequires, err
}

func UpdateInstalled(ctx context.Context, workspace string) ([]*Require, error) {
	modfile, err := readPath(workspace)
	if err != nil {
		return nil, err
	}

	repoNames := make([]string, 0, len(modfile.requires))

	for _, require := range modfile.requires {
		repoNames = append(repoNames, require.String())
	}

	return UpdateAll(ctx, workspace, repoNames)
}

func Ensure(workspace string) error {
	modfile, err := readPath(workspace)
	if err != nil {
		return err
	}

	return modfile.ensure()
}
