package engine

import (
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"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/httpz"
	"go.starlark.net/starlark"
	"go.temporal.io/sdk/temporal"
	"go.temporal.io/sdk/workflow"
)

var (
	_ flow.EventHandlerContext = (*flowContext)(nil)
)

type flowContext struct {
	ctx              workflow.Context
	activities       *activityState
	workflowIDPrefix string
}

func (f *flowContext) HTTPDo(reqURL string, method string, query url.Values, header http.Header, body []byte) (*flow.HTTPResponse, error) {
	u, err := url.Parse(reqURL)
	if err != nil {
		return nil, err
	}

	activityCtx := workflow.WithActivityOptions(f.ctx, workflow.ActivityOptions{
		// TODO
		StartToCloseTimeout: 10 * time.Second,
	})
	future := workflow.ExecuteActivity(activityCtx, f.activities.HTTPDoActivity, &HttpDoActivityInput{
		Method: method,
		Url:    httpz.MergeURLPathAndQuery(u, "", query),
		Header: toValuesMap(header),
		Body:   body,
	})
	var output HttpDoActivityOutput
	err = future.Get(f.ctx, &output)
	if err != nil {
		return nil, err
	}

	resp := toFlowHTTPResponse(&output)
	return &resp, nil
}

func (f *flowContext) Select(cases *starlark.Dict, defaultCase starlark.Value, timeout time.Duration, timeoutValue starlark.Value) (starlark.Value, error) {
	var selected starlark.Value
	selector := workflow.NewSelector(f.ctx)
	for kVal, v := range cases.Entries() {
		k, ok := kVal.(starlark.String)
		if !ok {
			return nil, fmt.Errorf("key must be a string, got %s", kVal.Type())
		}
		switch v := v.(type) {
		case *flow.FutureType:
			future, ok := v.Unwrap().(workflow.Future)
			if !ok {
				return nil, errors.New("internal error: future type did not return a wrapped workflow future")
			}
			selector.AddFuture(future, func(fut workflow.Future) {
				selected = k
			})
		case *flow.ReceiveChannelType:
			channel, ok := v.Unwrap().(workflow.ReceiveChannel)
			if !ok {
				return nil, errors.New("internal error: receive channel type did not return a wrapped workflow receive channel")
			}
			selector.AddReceive(channel, func(c workflow.ReceiveChannel, more bool) {
				selected = k
			})
		default:
			return nil, fmt.Errorf("unsupported value type %s for the select key %q", v.Type(), k)
		}
	}
	if defaultCase != starlark.None {
		selector.AddDefault(func() {
			selected = defaultCase
		})
	}
	if timeout != 0 {
		ctx, cancelFunc := workflow.WithCancel(f.ctx)
		defer cancelFunc()
		t := workflow.NewTimer(ctx, timeout)
		selector.AddFuture(t, func(f workflow.Future) {
			selected = timeoutValue
		})
	}
	selector.Select(f.ctx)
	return selected, nil
}

func (f *flowContext) Signal(workflowID, signalName, payloadJSON, runID string) (bool /* signaled */, error) {
	err := workflow.SignalExternalWorkflow(f.ctx, f.workflowIDPrefix+workflowID, runID, signalName, &SignalPayload{
		Payload: payloadJSON,
	}).Get(f.ctx, nil)
	switch err.(type) { //nolint:errorlint
	case nil:
		return true, nil
	case *temporal.UnknownExternalWorkflowExecutionError:
		return false, nil
	default:
		// TODO inspect other errors?
		return false, err
	}
}

func (f *flowContext) SignalChannel(signalName string) (flow.ReceiveChannel, error) {
	return &receiveChannel{
		ctx: f.ctx,
		c:   workflow.GetSignalChannel(f.ctx, signalName),
	}, nil
}

var (
	_ flow.ReceiveChannel = (*receiveChannel)(nil)
)

type receiveChannel struct {
	ctx workflow.Context
	c   workflow.ReceiveChannel
}

func (c *receiveChannel) Receive(payloadJSON *string) bool {
	return c.c.Receive(c.ctx, payloadJSON)
}

func (c *receiveChannel) Unwrap() any {
	return c.c
}

func (f *flowContext) Sleep(duration time.Duration) error {
	err := workflow.Sleep(f.ctx, duration)
	if err != nil {
		return err
	}
	return nil
}

func (f *flowContext) Timer(duration time.Duration) flow.Future {
	return &timer{
		ctx: f.ctx,
		f:   workflow.NewTimer(f.ctx, duration),
	}
}

var (
	_ flow.Future = (*timer)(nil)
)

type timer struct {
	ctx workflow.Context
	f   workflow.Future
}

func (t *timer) Get() error {
	return t.f.Get(t.ctx, nil)
}

func (t *timer) Unwrap() any {
	return t.f
}
