package module

import (
	"encoding/json"
	"errors"
	"unsafe"
)

type WorkflowError struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

func (e *WorkflowError) Error() string { return e.Code + ": " + e.Message }

func workflowRequest(kind, target string, args Value) ([]byte, error) {
	raw, err := args.MarshalJSON()
	if err != nil {
		return nil, err
	}
	request, err := MustObject(map[string]Value{"kind": String(kind), "target": String(target), "args": MustJSON(string(raw))}).MarshalJSON()
	if err != nil {
		return nil, err
	}
	// The host reads a call with internal/canonicaljson.Canonical, so a number
	// it would refuse is refused here, where the guest can still name it.
	if err := canonicalNumbers(request); err != nil {
		return nil, err
	}
	if len(request) > (64<<10)+4096 {
		return nil, errors.New("workflow call exceeds its byte limit")
	}
	return request, nil
}

func WorkflowCall(kind, target string, args Value) (Value, error) {
	request, err := workflowRequest(kind, target, args)
	if err != nil {
		return Value{}, err
	}
	capacity := 4096
	for {
		out := make([]byte, capacity)
		n := workflowCall(unsafe.Pointer(&request[0]), int32(len(request)), unsafe.Pointer(&out[0]), int32(len(out)))
		if n < 0 {
			capacity = int(-n)
			if capacity <= 0 || capacity > (64<<10)+4096 {
				return Value{}, errors.New("workflow response exceeds its byte limit")
			}
			continue
		}
		if n == 0 || int(n) > len(out) {
			return Value{}, errors.New("workflow host returned an invalid response")
		}
		var response struct {
			Outcome struct {
				Value json.RawMessage `json:"value"`
				Error *WorkflowError  `json:"error"`
			} `json:"outcome"`
		}
		if err := json.Unmarshal(out[:n], &response); err != nil {
			return Value{}, err
		}
		if response.Outcome.Error != nil {
			return Value{}, response.Outcome.Error
		}
		if len(response.Outcome.Value) == 0 {
			return Value{}, errors.New("workflow host returned no outcome")
		}
		value, err := parseWorkflowJSON(response.Outcome.Value)
		return value, err
	}
}

type FunctionOptions struct{ DatasetTransaction string }

func Function(name string, input Value, options ...FunctionOptions) (Value, error) {
	if len(options) > 1 {
		return Value{}, errors.New("Function accepts at most one options value")
	}
	args := map[string]Value{"input": input}
	if len(options) == 1 && options[0].DatasetTransaction != "" {
		args["dataset_transaction"] = String(options[0].DatasetTransaction)
	}
	return WorkflowCall("function", name, MustObject(args))
}
func Action(name string, input Value, target, candidateReceiptID *string) (Value, error) {
	targetValue, candidateValue := Null(), Null()
	if target != nil {
		targetValue = String(*target)
	}
	if candidateReceiptID != nil {
		candidateValue = String(*candidateReceiptID)
	}
	return WorkflowCall("action", name, MustObject(map[string]Value{
		"input": input, "target": targetValue, "candidate_receipt_id": candidateValue,
	}))
}
func StartWorkflow(name string, input Value) (Value, error) {
	return WorkflowCall("start_workflow", name, MustObject(map[string]Value{"input": input}))
}
func GetResult(runID string) (Value, error) {
	return WorkflowCall("get_result", "", MustObject(map[string]Value{"run_id": String(runID)}))
}

// CancelChild stops one child Workflow this Run started. A structured child's
// lifetime belongs to its parent, which also stops every outstanding child when
// it settles; already committed child Actions are never compensated.
func CancelChild(runID string) (Value, error) {
	return WorkflowCall("cancel_child", "", MustObject(map[string]Value{"run_id": String(runID)}))
}
func Sleep(milliseconds int64) (Value, error) {
	return WorkflowCall("sleep", "", MustObject(map[string]Value{"milliseconds": Integer(milliseconds)}))
}

// SignalOptions fences a wait after an earlier Action receipt and/or at an
// absolute business deadline. The host verifies the receipt belongs to this run.
type SignalCheck struct {
	Function string
	Input    Value
}
type SignalOptions struct {
	After, DeadlineAt string
	Until             *SignalCheck
}

func Recv(name, subject string, timeoutMS int64, options ...SignalOptions) (Value, error) {
	if len(options) > 1 {
		return Value{}, errors.New("Recv accepts at most one options value")
	}
	args := map[string]Value{"subject": String(subject), "timeout_ms": Integer(timeoutMS)}
	if len(options) == 1 {
		if options[0].After != "" {
			args["after"] = String(options[0].After)
		}
		if options[0].DeadlineAt != "" {
			args["deadline_at"] = String(options[0].DeadlineAt)
		}
		if options[0].Until != nil {
			args["until"] = MustObject(map[string]Value{"function": String(options[0].Until.Function), "input": options[0].Until.Input})
		}
	}
	return WorkflowCall("recv", name, MustObject(args))
}
func Now() (Value, error) { return WorkflowCall("now", "", MustObject(map[string]Value{})) }
func Random(bytes int64) (Value, error) {
	return WorkflowCall("random", "", MustObject(map[string]Value{"bytes": Integer(bytes)}))
}
func Race(runIDs []string) (Value, error) {
	values := make([]Value, len(runIDs))
	for i, id := range runIDs {
		values[i] = String(id)
	}
	return WorkflowCall("race", "", MustObject(map[string]Value{"run_ids": List(values...)}))
}

// Complete selects a declared terminal alternative with typed result fields.
func Complete(outcome string, result Value) Value {
	return MustObject(map[string]Value{"outcome": String(outcome), "result": result})
}
