package flow

import (
	"context"
	"errors"
	"fmt"
	"time"

	starjson "go.starlark.net/lib/json"
	starmath "go.starlark.net/lib/math"
	startime "go.starlark.net/lib/time"
	"go.starlark.net/starlark"
	"go.starlark.net/syntax"
)

const (
	timeBuiltin = "time"
	jsonBuiltin = "json"
	mathBuiltin = "math"
)

// runtimeConfig holds static stateless definitions for all Flow programs.
type runtimeConfig struct {
	fileOpts    *syntax.FileOptions
	predeclared starlark.StringDict
}

func newRuntimeConfig() *runtimeConfig {
	fileOpts := &syntax.FileOptions{
		Set:             true,
		While:           true,
		TopLevelControl: true,
		GlobalReassign:  false,
		Recursion:       false,
	}

	return &runtimeConfig{
		fileOpts: fileOpts,
		predeclared: starlark.StringDict{
			timeBuiltin:    startime.Module,
			jsonBuiltin:    starjson.Module,
			mathBuiltin:    starmath.Module,
			onEventBuiltin: starlark.NewBuiltin(onEventBuiltin, onEventBuiltinFunc),
			sourceBuiltin:  starlark.NewBuiltin(sourceBuiltin, sourceBuiltinFunc),
		},
	}
}

type Runtime struct {
	opts  RuntimeOptions
	state *runtimeState
}

type runtimeState struct {
	modules       []ModuleSource
	eventHandlers map[string][]eventHandler
}

func NewRuntimeFromSource(ctx context.Context, opts RuntimeOptions) (*Runtime, error) {
	rc := newRuntimeConfig()
	state, err := rc.loadFlow(ctx, opts)
	if err != nil {
		return nil, err
	}
	return &Runtime{
		opts:  opts,
		state: state,
	}, nil
}

func NewRuntimeFromPreloadedSource(ctx context.Context, opts PreloadedRuntimeOptions) (*Runtime, error) {
	return NewRuntimeFromSource(ctx, RuntimeOptions{
		ProjectPath: opts.ProjectPath,
		Entrypoint:  opts.Entrypoint,
		Builtins:    opts.Builtins,

		Source: &preloadedSource{
			modules: opts.Modules,
		},
	})
}

type PreloadedRuntimeOptions struct {
	ProjectPath string
	Entrypoint  string
	Builtins    Builtins

	Modules []ModuleSource
}

type RuntimeOptions struct {
	ProjectPath string
	Entrypoint  string
	Builtins    Builtins

	Source Source
}

// loadFlow is used from workflows and from activities so must be extra explicit about what inputs it takes
// and what objects it uses.
func (r *runtimeConfig) loadFlow(ctx context.Context, lfo RuntimeOptions) (*runtimeState, error) {
	recSrc := newRecordingSource(lfo.Source)
	oes := &onEventState{
		handlers: map[string][]eventHandler{},
	}
	opts := loaderOptions{
		Source: recSrc,
		Print: func(thread *starlark.Thread, msg string) {
			lfo.Builtins.Print(msg)
		},
		Now:         lfo.Builtins.Now,
		Predeclared: r.predeclared,
		Locals: map[string]any{
			localOnEventState: oes,
			localSource:       recSrc,
		},
		FileOptions: r.fileOpts,
	}
	_, err := opts.Load(ctx, lfo.ProjectPath, lfo.Entrypoint)
	if err != nil {
		return nil, err
	}

	return &runtimeState{
		eventHandlers: oes.handlers,
		modules:       recSrc.modules,
	}, nil
}

func (r *Runtime) LoadedModules() []ModuleSource {
	return r.state.modules
}

func (r *Runtime) EvaluateEventHandlerIDs(typ string, event *starlark.Dict) ([]string, error) {
	eh := r.state.eventHandlers[typ]
	handlers := make([]string, 0, len(eh))
	for i, h := range eh {
		id := ""
		switch idStar := h.workflowID.(type) {
		case starlark.String:
			id = string(idStar)
		case *starlark.Function:
			var err error
			id, err = r.evaluateEventHandlerName(i, idStar, starlark.Tuple{event})
			if err != nil {
				return nil, err
			}
		default:
			return nil, fmt.Errorf("unexpected workflow_id type: %T", h.workflowID)
		}
		handlers = append(handlers, id)
	}
	return handlers, nil
}

func (r *Runtime) Execute(eventType string, eventHandlerID uint32, ctx EventHandlerContext, vars starlark.StringDict, event *starlark.Dict) error {
	handlers := r.state.eventHandlers[eventType]
	if int(eventHandlerID) >= len(handlers) {
		return fmt.Errorf("internal error: want to run handler %d, but loaded only %d", eventHandlerID, len(handlers))
	}

	thread := &starlark.Thread{
		Name:  fmt.Sprintf("Handler %d", eventHandlerID),
		Print: r.safePrint,
		Load:  unexpectedLoad,
	}
	startime.SetNow(thread, func() (time.Time, error) {
		return r.opts.Builtins.Now(), nil
	})
	h := handlers[eventHandlerID]

	m := &eventHandlerContextModule{ctx: ctx, vars: vars}
	args := starlark.Tuple{m.toStarlarkModule(), event}
	ret, err := starlark.Call(thread, h.handler, args, nil)
	if err != nil {
		return fmt.Errorf("handler failed: %w", err)
	}
	if ret != starlark.None {
		return errors.New("handler must not return anything")
	}
	return nil
}

func (r *Runtime) evaluateEventHandlerName(eventHandlerID int, f *starlark.Function, args starlark.Tuple) (string, error) {
	thread := &starlark.Thread{
		Name:  fmt.Sprintf("Construct workflow ID for handler %d", eventHandlerID),
		Print: r.safePrint,
		Load:  unexpectedLoad,
	}
	startime.SetNow(thread, func() (time.Time, error) {
		return r.opts.Builtins.Now(), nil
	})
	ret, err := starlark.Call(thread, f, args, nil)
	if err != nil {
		return "", err
	}

	idStr, ok := ret.(starlark.String)
	if !ok {
		return "", fmt.Errorf("workflow_id must return a string, got %T", ret)
	}
	id := string(idStr)

	return id, nil
}

func (r *Runtime) safePrint(thread *starlark.Thread, msg string) {
	msg = maskSensitiveGitLabValues(msg)
	r.opts.Builtins.Print(msg)
}

func unexpectedLoad(thread *starlark.Thread, module string) (starlark.StringDict, error) {
	return nil, fmt.Errorf("unexpected load(%s) call", module)
}
