package engine

import (
	"context"
	"fmt"
	"maps"
	"time"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/autoflow/flow"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
	"go.starlark.net/starlark"
	enumspb "go.temporal.io/api/enums/v1"
	"go.temporal.io/sdk/workflow"
)

// workflowState holds shared state that is used by all Temporal workflows.
type workflowState struct {
	activities *activityState
	vars       starlark.StringDict
}

func (w *workflowState) HandleEventWorkflow(ctx workflow.Context, input *HandleEventWorkflowInput) (*HandleEventWorkflowOutput, error) {
	log := workflow.GetLogger(ctx)
	log.Info("Started handle event workflow")

	activityCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
		// TODO
		StartToCloseTimeout: 10 * time.Second,
	})

	f := workflow.ExecuteActivity(activityCtx, w.activities.LoadAllEventHandlersActivity, &LoadAllEventHandlersActivityInput{
		Event:       input.Event,
		FlowProject: input.FlowProject,
		Entrypoint:  input.Entrypoint,
	})
	var output LoadAllEventHandlersActivityOutput
	err := f.Get(ctx, &output)
	if err != nil {
		// NOTE: we log our own log message because of
		// https://github.com/temporalio/sdk-go/issues/1808#issuecomment-2662368441
		log.Error("LoadAllEventHandlersActivity failed", logz.Error(err))
		return &HandleEventWorkflowOutput{}, err
	}

	nHandlers := len(output.Handlers)
	if nHandlers == 0 {
		log.Info("No handlers registered for event type")
		return nil, nil
	}
	log.Info(fmt.Sprintf("Starting event handler workflows for %d registered event handlers", nHandlers))

	// We spawn a child workflow for each event handler, but we don't want to block this workflow until those handlers
	// finish executing as that may take an arbitrary amount of time. So we:
	// 1. Set ParentClosePolicy to PARENT_CLOSE_POLICY_ABANDON to avoid canceling child workflows when
	//    parent (i.e. this workflow) exits.
	// 2. Run ExecuteChildWorkflow() for each handler.
	// 3. Wait for all child workflows to emit ChildWorkflowExecutionStarted event. Must wait for the event, otherwise
	//    child workflows don't start.

	futures := make(map[string]workflow.Future, nHandlers)
	workflowIDPrefix := fmt.Sprintf("project_%d/", input.FlowProject.Id)
	for handler, h := range output.Handlers {
		id := h.WorkflowId
		if id != "" {
			id = workflowIDPrefix + id
		}
		childCtx := workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{
			// TODO
			WorkflowID:        id,
			ParentClosePolicy: enumspb.PARENT_CLOSE_POLICY_ABANDON,
		})
		fut := workflow.ExecuteChildWorkflow(childCtx, w.RunEventHandlerWorkflow, &RunEventHandlerWorkflowInput{
			Event:            input.Event,
			FlowProject:      input.FlowProject,
			Variables:        input.Variables,
			Entrypoint:       input.Entrypoint,
			Modules:          output.Modules,
			Handler:          uint32(handler), //nolint: gosec
			WorkflowIdPrefix: workflowIDPrefix,
		})
		futures[id] = fut.GetChildWorkflowExecution()
	}
	// There is no determinism problem with iterating over this particular map
	// since we don't rely on any particular ordering.
	//workflowcheck:ignore
	for id, fut := range futures {
		err = fut.Get(ctx, nil)
		if err != nil {
			// NOTE: we log our own log message because of
			// https://github.com/temporalio/sdk-go/issues/1808#issuecomment-2662368441
			log.Error("Failed to start event handler workflow", logz.Error(err), logz.EventHandler(id))
			return nil, err
		}
	}
	return &HandleEventWorkflowOutput{}, nil
}

func (w *workflowState) RunEventHandlerWorkflow(ctx workflow.Context, input *RunEventHandlerWorkflowInput) (*RunEventHandlerWorkflowOutput, error) {
	log := workflow.GetLogger(ctx)
	log.Info("Started event handler workflow")

	// We are loading the flow runtime from preloaded modules,
	// thus it's safe that this call is deterministic.
	//workflowcheck:ignore
	runtime, err := flow.NewRuntimeFromPreloadedSource(context.Background(), flow.PreloadedRuntimeOptions{
		Modules:     toFlowModuleSources(input.Modules),
		ProjectPath: input.FlowProject.FullPath,
		Entrypoint:  input.Entrypoint,
		Builtins: &flowBuiltins{
			printFunc: func(msg string) {
				log.Info(msg, logz.IsScriptPrint())
			},
			nowFunc: func() time.Time {
				return workflow.Now(ctx)
			},
		},
	})
	if err != nil {
		// NOTE: we log our own log message because of
		// https://github.com/temporalio/sdk-go/issues/1808#issuecomment-2662368441
		log.Error("Failed to created flow runtime", logz.Error(err))
		return nil, err
	}

	fc := &flowContext{
		ctx:              ctx,
		activities:       w.activities,
		workflowIDPrefix: input.WorkflowIdPrefix,
	}
	e, err := EventToDict(input.Event)
	if err != nil {
		// NOTE: we log our own log message because of
		// https://github.com/temporalio/sdk-go/issues/1808#issuecomment-2662368441
		log.Error("Failed to convert event object to starlark dict", logz.Error(err))
		return nil, err
	}

	// TODO: split pre-defined variables from event variables.
	// Maybe we should define some kind of a global object in starlark that contains
	// those pre-defined "constants" (yes, semantically they are constants).
	variables := maps.Clone(w.vars) // we are okay with a shallow copy here.
	// The for-loop below is okay (w.r.t determinism concerns), because downstream won't care about order.
	//workflowcheck:ignore
	for k, v := range input.Variables {
		if _, ok := variables[k]; !ok { // we don't want to overwrite KAS variables
			variables[k] = starlark.String(v)
		}
	}

	// This call to runtime.Execute is "safe" to treat as deterministic,
	// because that is what Flow / Starlark guarantees us.
	//workflowcheck:ignore
	err = runtime.Execute(input.Event.Type, input.Handler, fc, variables, e)
	if err != nil {
		// NOTE: we log our own log message because of
		// https://github.com/temporalio/sdk-go/issues/1808#issuecomment-2662368441
		log.Error("Failed to run workflow event handler", logz.Error(err))
		return nil, err
	}

	return &RunEventHandlerWorkflowOutput{}, nil
}
