package flow

import (
	"context"
	"errors"
	"path"
	"time"

	startime "go.starlark.net/lib/time"
	"go.starlark.net/starlark"
	"go.starlark.net/syntax"
)

type loaderEntry struct {
	globals starlark.StringDict
	err     error
}

type loaderOptions struct {
	Source      Source
	Print       func(thread *starlark.Thread, msg string)
	Now         func() time.Time
	Predeclared starlark.StringDict
	Locals      map[string]any
	FileOptions *syntax.FileOptions
}

type loader struct {
	loaderOptions
	ctx      context.Context //nolint:containedctx
	cache    map[ModuleRef]*loaderEntry
	rootPath string
}

func (lo loaderOptions) Load(ctx context.Context, projectPath, entrypoint string) (starlark.StringDict, error) {
	dir, file := path.Split(entrypoint)
	l := loader{
		loaderOptions: lo,
		ctx:           ctx,
		cache:         make(map[ModuleRef]*loaderEntry),
		rootPath:      dir,
	}
	thread := &starlark.Thread{
		Name:  "Load " + entrypoint,
		Print: lo.Print,
		Load:  l.load,
	}
	for k, v := range lo.Locals {
		thread.SetLocal(k, v)
	}
	startime.SetNow(thread, func() (time.Time, error) {
		return lo.Now(), nil
	})
	lo.Source.SetupThreadLocal(thread, projectPath)
	return l.load(thread, file)
}

// if module is not an absolute path, it's a path relative to the entrypoint module.
func (l *loader) load(thread *starlark.Thread, module string) (starlark.StringDict, error) {
	ref, err := parseModuleRef(l.rootPath, module)
	if err != nil {
		return nil, err
	}
	e, ok := l.cache[ref]
	if e == nil {
		if ok {
			// request for package whose loading is in progress
			return nil, errors.New("cycle in load() graph")
		}

		// Add a placeholder to indicate "load in progress".
		l.cache[ref] = nil

		// Load the module source.
		moduleData, err := l.Source.LoadFile(l.ctx, thread, ref)
		if err != nil {
			return nil, err
		}

		// Load the module.
		globals, err := starlark.ExecFileOptions(l.FileOptions, thread, ref.String(), moduleData, l.Predeclared)
		e = &loaderEntry{
			globals: globals,
			err:     err,
		}
		var evalErr *starlark.EvalError
		if err != nil && errors.As(err, &evalErr) {
			l.Print(thread, evalErr.Error()+"\n"+evalErr.Backtrace())
		}

		// Store in the cache.
		l.cache[ref] = e
	}
	return e.globals, e.err
}
