



text := import("text")

ll := import("@platforma-sdk/workflow-tengo:ll")
validation := import("@platforma-sdk/workflow-tengo:validation")
feats := import("@platforma-sdk/workflow-tengo:feats")
oop := import("@platforma-sdk/workflow-tengo:oop")

maps := import("@platforma-sdk/workflow-tengo:maps")
smart := import("@platforma-sdk/workflow-tengo:smart")
json := import("json")
constants := import("@platforma-sdk/workflow-tengo:exec.constants")
slices := import("@platforma-sdk/workflow-tengo:slices")
limits := import("@platforma-sdk/workflow-tengo:exec.limits")

delayedCompAlloc := import("@platforma-sdk/workflow-tengo:exec.delayed-compute-allocation")

_RTYPE_RUN_COMMAND_V1 := { Name: "RunCommand/executor", Version: "1" }
_RTYPE_RUN_COMMAND_CMD := { Name: "RunCommandCmd", Version: "1" }
_RTYPE_RUN_COMMAND_ARGS := { Name: "RunCommandArgs", Version: "1" }
_RTYPE_RUN_COMMAND_OPTIONS := { Name: "run-command/options", Version: "1" }
_RTYPE_RUN_COMMAND_REFS := { Name: "RunCommandRefs", Version: "1" }

ARG_SCHEMA := {
	"__options__,closed": true,
	"type": "string,regex=string|expressionRef|variableRef|secret",
	"value": "string"
}

RUN_CMD_PLAN_REFS_SCHEMA := "any"

RUN_CMD_PLAN_OPTIONS_SCHEMA := {
	"cmd,?": ARG_SCHEMA,
	"args": [ARG_SCHEMA],
	"envs": { "any": ARG_SCHEMA },
	"substRules": { "any": "string" },
	"customPaths": [ARG_SCHEMA],
	"dockerImageTag,?": "string",
	"dockerEntrypoint,?": ["string"]
}

RUN_CMD_PLAN_STATE_SCHEMA := {
	"refs": RUN_CMD_PLAN_REFS_SCHEMA,
	"options": RUN_CMD_PLAN_OPTIONS_SCHEMA
}

_isRunCommandPlanObject := func(obj) {
	return ll.isStrict(obj) && maps.containsKey(obj, "_type") && obj["_type"] == "runcmd.plan"
}

_validateIsRunCommandPlanObject := func(obj, keyPath, stack) {
	if !_isRunCommandPlanObject(obj) {
		return validation.fail(keyPath, "item %q is not a run command plan object", obj)
	}

	return validation.success
}

RUN_CMD_PLAN_STATE_OR_OBJECT_SCHEMA := [ "or", RUN_CMD_PLAN_STATE_SCHEMA, _validateIsRunCommandPlanObject ]

_initStateFromPlan := func(state, initState, context) {
	validation.assertType(initState, RUN_CMD_PLAN_STATE_SCHEMA, "invalid initial state for " + context)

	initOptions := initState.options
	state.options.cmd = maps.clone(initOptions.cmd)
	state.options.args = maps.clone(initOptions.args)
	state.options.envs = maps.clone(initOptions.envs)
	state.options.substRules = maps.clone(initOptions.substRules)
	state.options.customPaths = maps.clone(initOptions.customPaths)
	state.options.dockerImageTag = initOptions.dockerImageTag
	state.options.dockerEntrypoint = maps.clone(initOptions.dockerEntrypoint)

	for k, v in initState.refs {
		state.refs[k] = v
	}
}


_FIELD_ALLOCATION := "allocation" // not in v1
_FIELD_REFS := "refs"
_FIELD_CMD := "cmd"
_FIELD_ARGS := "args"
_FIELD_OPTIONS := "options"
_FIELD_WORKDIR_IN := "workdirIn"


_FIELD_WORKDIR_OUT := "workdirOut"






simpleArg := func(argument) {
	return {
		type: constants.ARG_TYPE_STRING,
		value: argument
	}
}






variableArg := func(argTpl) {
	return {
		type: constants.ARG_TYPE_VAR,
		value: argTpl
	}
}







expressionArg := func(argTpl) {
	return {
		type: constants.ARG_TYPE_EXPRESSION,
		value: argTpl
	}
}





secretArg := func(secretName) {
	return {
		type: constants.ARG_TYPE_SECRET,
		value: secretName
	}
}

_createAbstractRunCommandPlan := func(state, returnSelfFn) {
	self := undefined

	self = ll.toStrict({
		_type: "runcmd.plan",






		cmd: func(cmd) {
			if ll.isMap(cmd) {
				self.cmdTyped(cmd.type, cmd.value)
			} else {
				state.options.cmd = simpleArg(cmd)
			}
			return returnSelfFn()
		},









		cmdVar: func(commandTpl) {
			state.options.cmd = variableArg(commandTpl)
			return returnSelfFn()
		},









		cmdExpression: func(commandTpl) {
			state.options.cmd = expressionArg(commandTpl)
			return returnSelfFn()
		},

		cmdTyped: func(type, value) {
			ll.assert(slices.hasElement(constants.ARG_TYPES, type),
				"runcmd.plan.cmdTyped: unknown type %s, expect: %v", type, constants.ARG_TYPES)
			ll.assert(type != constants.ARG_TYPE_SECRET,
				"runcmd.plan.cmdTyped: 'secret' type is only valid for environment variables")

			if type == constants.ARG_TYPE_STRING {
				return self.cmd(value)
			} else if type == constants.ARG_TYPE_EXPRESSION {
				return self.cmdExpression(value)
			} else if type == constants.ARG_TYPE_VAR {
				return self.cmdVar(value)
			}

			ll.panic("runcmd.plan.cmdTyped: unknown type %s, expect: %v", type, constants.ARG_TYPES)
		},






		arg: func(arg) {
			if ll.isMap(arg) {
				self.argTyped(arg.type, arg.value)
			} else {
				state.options.args = append(state.options.args, simpleArg(arg))
			}
			return returnSelfFn()
		},









		argVar: func(argTpl) {
			state.options.args = append(state.options.args, variableArg(argTpl))
			return returnSelfFn()
		},









		argExpression: func(argTpl) {
			state.options.args = append(state.options.args, expressionArg(argTpl))
			return returnSelfFn()
		},

		argTyped: func(type, value) {
			ll.assert(slices.hasElement(constants.ARG_TYPES, type),
				"runcmd.plan.argTyped: unknown type %s, expect: %v", type, constants.ARG_TYPES)
			ll.assert(type != constants.ARG_TYPE_SECRET,
				"runcmd.plan.argTyped: 'secret' type is only valid for environment variables")

			if type == constants.ARG_TYPE_STRING {
				return self.arg(value)
			} else if type == constants.ARG_TYPE_EXPRESSION {
				return self.argExpression(value)
			} else if type == constants.ARG_TYPE_VAR {
				return self.argVar(value)
			}

			ll.panic("runcmd.plan.argTyped: unknown type %s, expect: %v", type, constants.ARG_TYPES)
		},




		resetArgs: func() {
			state.options.args = []
			return returnSelfFn()
		},







		ref: func(refKey, refOrStr) {
			ref := refOrStr
			if is_string(refOrStr) {
				ref = smart.createJsonResource(refOrStr)
			}

			ll.assert(smart.isReference(ref),
				"runcmd.plan.ref() must be a reference (valid field or resource) %s", refKey)

			ll.assert(!maps.containsKey(state.refs, refKey),
				"attempt to override existing reference %q", refKey)

			state.refs[refKey] = ref
			return returnSelfFn()
		},










		refVar: func(varName, refKey, refOrStr) {
			self.ref(refKey, refOrStr)
			self.substitutionRule(varName, refKey)
			return returnSelfFn()
		},







		env: func(name, value) {
			if ll.isMap(value) {
				self.envTyped(name, value.type, value.value)
			} else {
				state.options.envs[name] = simpleArg(value)
			}
			return returnSelfFn()
		},








		envVar: func(name, valueTpl) {
			state.options.envs[name] = variableArg(valueTpl)
			return returnSelfFn()
		},








		envExpression: func(name, valueTpl) {
			state.options.envs[name] = expressionArg(valueTpl)
			return returnSelfFn()
		},







		envSecret: func(name, secretName) {
			state.options.envs[name] = secretArg(secretName)
			return returnSelfFn()
		},

		envTyped: func(name, type, value) {
			ll.assert(slices.hasElement(constants.ARG_TYPES, type),
				"runcmd.plan.envTyped: unknown type %s, expect: %v", type, constants.ARG_TYPES)

			if type == constants.ARG_TYPE_STRING {
				return self.env(name, value)
			} else if type == constants.ARG_TYPE_EXPRESSION {
				return self.envExpression(name, value)
			} else if type == constants.ARG_TYPE_VAR {
				return self.envVar(name, value)
			} else if type == constants.ARG_TYPE_SECRET {
				return self.envSecret(name, value)
			}

			ll.panic("runcmd.plan.envTyped: unknown type %s, expect: %v", type, constants.ARG_TYPES)
		},






		addPath: func(path) {
			if ll.isMap(path) {
				self.addPathTyped(path.type, path.value)
			} else {
				state.options.customPaths = append(state.options.customPaths, simpleArg(path))
			}
			return returnSelfFn()
		},







		addPathVar: func(pathTpl) {
			state.options.customPaths = append(state.options.customPaths, variableArg(pathTpl))
			return returnSelfFn()
		},







		addPathExpression: func(pathTpl) {
			state.options.customPaths = append(state.options.customPaths, expressionArg(pathTpl))
			return returnSelfFn()
		},

		addPathTyped: func(type, value) {
			ll.assert(slices.hasElement(constants.ARG_TYPES, type),
				"runcmd.plan.addPathTyped: unknown type %s, expect: %v", type, constants.ARG_TYPES)
			ll.assert(type != constants.ARG_TYPE_SECRET,
				"runcmd.plan.addPathTyped: 'secret' type is only valid for environment variables")

			if type == constants.ARG_TYPE_STRING {
				return self.addPath(value)
			} else if type == constants.ARG_TYPE_EXPRESSION {
				return self.addPathExpression(value)
			} else if type == constants.ARG_TYPE_VAR {
				return self.addPathVar(value)
			}

			ll.panic("runcmd.plan.addPathTyped: unknown type %s, expect: %v", type, constants.ARG_TYPES)
		},








		substitutionRule: func(varName, refKey) {
			validation.assert(!text.contains(varName, "-"), "runcmd.plan.substitutionRule: varName must not contain '-' character: it is interpreted as 'minus' sign")

			state.options.substRules[varName] = refKey
			return returnSelfFn()
		},






		substitutionRules: func(rules) {
			for k, v in rules {
				state.options.substRules[k] = v
			}
			return returnSelfFn()
		},






		dockerImageTag: func(tag) {
			state.options.dockerImageTag = tag
			return returnSelfFn()
		},






		dockerEntrypoint: func(entrypoint) {
			state.options.dockerEntrypoint = maps.clone(entrypoint)
			return returnSelfFn()
		}
	})

	return self
}

createRunCommandPlan := func(...initialState) {
	state := {
		refs: {},
		options: {
			cmd: undefined,
			args: [],
			envs: {},
			substRules: {},
			customPaths: [],
			dockerImageTag: undefined,
			dockerEntrypoint: []
		}
	}

	if len(initialState) == 1 {
		initState := initialState[0]
		_initStateFromPlan(state, initState, "run command plan")
	} else {
		ll.assert(len(initialState) == 0, "createRunCommandPlan accepts at most one argument")
	}

	self := undefined

	self = oop.inherit(_createAbstractRunCommandPlan(state, func() { return self }), {
		state: func() {
			newState := {
				options: maps.clone(state.options),
				refs: {}
			}
			for k, v in state.refs {
				newState.refs[k] = v
			}
			return newState
		}
	})

	return self
}







builder := func(workdir, ...initialState) {
	self := undefined

	state := {
		refs: {},
		options: {
			cmd: undefined,
			args: [],
			envs: {},
			substRules: {},
			customPaths: [],
			dockerImageTag: "",
			dockerEntrypoint: []
		}
	}

	if len(initialState) == 1 {
		initState := initialState[0]
		if (_isRunCommandPlanObject(initState)) {
			initState = initState.state()
		}
		_initStateFromPlan(state, initState, "run command builder")
	} else {
		ll.assert(len(initialState) == 0, "builder accepts at most two arguments")
	}

	allocation := undefined
	queue := undefined
	cpu := undefined
	ram := undefined

	commandName := ""
	stdout := "stdout.txt"
	stderr := "stderr.txt"
	nErrorLines := 200

	self = oop.inherit(_createAbstractRunCommandPlan(state, func() { return self }), {






		name: func(name) {
			validation.assertType(name, "string", "runcmd.builder.name: name must be a string")

			commandName = name
			return self
		},






		allocation: func(value) {
			validation.assertType(value, validation.reference, "runcmd.builder: allocation should be a valid reference")
			allocation = value
			return self
		},






		request: func(request) {
			validation.assertType(request, {
				"queue": "string",
				"cpu,?": "number",
				"ram,?": ["or", "number", "string"]
			}, "runcmd.builder.request: request must be a valid request")

			queue = request.queue
			cpu = request.cpu
			ram = request.ram

			return self
		},






		inQueue: func(queueName) {
			queue = queueName
			return self
		},






		mem: func(value) {
			validation.assertType(value, ["or", "number", "string"], "exec.builder.mem: RAM amount should be a number or string")
			ll.assert(is_string(value) || value > 0, "exec.builder.mem: amount in bytes should be greater than 0")
			ram = value
			return self
		},






		cpu: func(value) {
			validation.assertType(value, "number", "runcmd.builder.cpu: value must be a number")
			cpu = value
			return self
		},






		stdout: func(fileName) {
			stdout = fileName
			return self
		},






		stderr: func(fileName) {
			stderr = fileName
			return self
		},






		nErrorLines: func(number) {
			nErrorLines = number
			return self
		},






		build: func() {
			runRes := undefined
			options := state.options
			refs := state.refs

			hasPatternArgs := false
			hasExpressionArgs := false
			hasSecretArgs := false

			check := func(arg) {
				if !ll.isMap(arg) {
					return
				}
				if arg.type == constants.ARG_TYPE_VAR {
					hasPatternArgs = true
				}
				if arg.type == constants.ARG_TYPE_EXPRESSION {
					hasExpressionArgs = true
				}
				if arg.type == constants.ARG_TYPE_SECRET {
					hasSecretArgs = true
				}
			}

			check(options.cmd)

			for arg in options.args {
				check(arg)
			}

			for _, env in options.envs {
				check(env)
			}

			for path in options.customPaths {
				check(path)
			}




			hasAllocation := !is_undefined(allocation)
			hasCpuRamRequest := !is_undefined(cpu) || !is_undefined(ram)

			ll.assert(!(hasAllocation && hasCpuRamRequest), // none specified is OK: default queue limits would be applied.
				"runcmd.builder.build: allocation and CPU/RAM request are mutually exclusive for the same command run")

			ll.assert(!is_undefined(queue), "runcmd.builder.build: queue is required for any command run")

			resourceTypes := limits.getResourceTypes(queue)
			runRes = smart.ephemeralBuilder(resourceTypes.runCommand)

			runRes.getField(_FIELD_WORKDIR_IN).set(workdir)



			ll.assert(!hasPatternArgs || len(options.substRules) != 0,
				"found pattern argument, but no substitution rules were set: hasPatternArgs: %v, substRules: %v, cmd: %v, args: %v, envs: %v",
				hasPatternArgs, options.substRules, options.cmd, options.args, options.envs)
			ll.assert(!hasExpressionArgs || feats.commandExpressions,
				"runcmd.builder().build: expression arguments must be enabled only if platforma backend supports expressions, found: %v",
				hasExpressionArgs)
			ll.assert(!hasSecretArgs || feats.secretEnvSupport,
				"runcmd.builder().build: secret arguments must be enabled only if platforma backend supports secret env type")

			if !is_undefined(allocation) {
				runRes.getField(_FIELD_ALLOCATION).set(allocation)
			} else {
				request := {
					queue: queue
				}
				if !is_undefined(cpu) {
					request.cpu = cpu
				}
				if !is_undefined(ram) {
					request.ram = ram
				}






				runRes.getField(_FIELD_ALLOCATION).set(delayedCompAlloc.create(workdir, request))
			}

			refsRes := smart.createMapResourceWithType(_RTYPE_RUN_COMMAND_REFS, refs)
			runRes.getField(_FIELD_REFS).set(refsRes)

			cmdRes := smart.createValueResource(_RTYPE_RUN_COMMAND_CMD, json.encode(options.cmd))
			runRes.getField(_FIELD_CMD).set(cmdRes)

			buildOptions := {
				name: commandName,
				queueName: queue,
				errorLines: nErrorLines,
				redirectStdout: stdout,
				redirectStderr: stderr,
				env: options.envs,
				additionalPaths: options.customPaths,
				substitutions: options.substRules,
				dockerImageTag: options.dockerImageTag,
				dockerEntrypoint: options.dockerEntrypoint
			}

			optionsRes := smart.createValueResource(_RTYPE_RUN_COMMAND_OPTIONS, json.encode(buildOptions))
			runRes.getField(_FIELD_OPTIONS).set(optionsRes)

			argsRes := smart.createValueResource(_RTYPE_RUN_COMMAND_ARGS, json.encode(options.args))
			runRes.getField(_FIELD_ARGS).set(argsRes)

			res := oop.inherit(runRes.lockAndBuild(), {
				"workdir": runRes.outputs()[_FIELD_WORKDIR_OUT],
				"options": buildOptions
			})

			return res
		}
	})

	return self
}

export ll.toStrict({

	ARG_SCHEMA:                             ARG_SCHEMA,
	RUN_CMD_PLAN_REFS_SCHEMA:               RUN_CMD_PLAN_REFS_SCHEMA,
	RUN_CMD_PLAN_OPTIONS_SCHEMA:            RUN_CMD_PLAN_OPTIONS_SCHEMA,
	RUN_CMD_PLAN_STATE_SCHEMA:              RUN_CMD_PLAN_STATE_SCHEMA,
	RUN_CMD_PLAN_STATE_OR_OBJECT_SCHEMA:    RUN_CMD_PLAN_STATE_OR_OBJECT_SCHEMA,


	builder: builder,
	simpleArg: simpleArg,
	variableArg: variableArg,
	expressionArg: expressionArg,
	secretArg: secretArg,
	createRunCommandPlan: createRunCommandPlan
})
