



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

assets := import("@platforma-sdk/workflow-tengo:assets")
smart := import("@platforma-sdk/workflow-tengo:smart")
times := import("times")
maps := import("@platforma-sdk/workflow-tengo:maps")
sets := import("@platforma-sdk/workflow-tengo:sets")
slices := import("@platforma-sdk/workflow-tengo:slices")
path := import("@platforma-sdk/workflow-tengo:path")
render := import("@platforma-sdk/workflow-tengo:render")
enum := import("enum")
oop := import("@platforma-sdk/workflow-tengo:oop")
constants := import("@platforma-sdk/workflow-tengo:constants")
limits := import("@platforma-sdk/workflow-tengo:exec.limits")
monetization := import("@platforma-sdk/workflow-tengo:exec.monetization_internal")
execConstants := import("@platforma-sdk/workflow-tengo:exec.constants")






builder := func() {
	self := undefined

	soft := undefined
	cmd := undefined
	args := [] // list of strings
	argsTyped := [] // list of {type: expression|string, value: string}
	typedArgSet := false
	typedEnvSet := false
	envs := {} // map of names to values-strings
	envsTyped := {} // map of names to {type: expression|string, value: string}
	secrets := {} // { "env_name": "{secrets.secret_name}", ... }

	entrypointSet := false

	assertEntrypointNotSet := func() {
		ll.assert(!entrypointSet,
			"either a command with arguments or a software were already set.")
	}




	queue := execConstants.HEAVY_QUEUE
	cpuRequest := undefined
	ramRequest := undefined

	cache := 0
	nErrorLines := 200


	filesToAdd := {}

	filesToWrite := {}

	dirsToCreate := {}


	filesToSave := {}

	filesContentToSave := {}

	fileSetsToSave := {}

	fileSetsContentToSave := {}

	filesToStream := {}

	wdProcessors := {}


	assetInfos := {}
	assetDescriptors := {}
	assetRules := {}
	assetCache := {}

	mnz := monetization.init()

	stdout := "stdout.txt"
	stderr := "stderr.txt"

	stdoutToValue := false
	stderrToValue := false
	stderrToStdout := false

	self = ll.toStrict({





		cmd: func(commandName) {
			assertEntrypointNotSet()

			cmd = commandName
			entrypointSet = true
			return self
		},






		software: func(sw) {
			assertEntrypointNotSet()
			ll.assert(assets.isSoftwareInfo(sw),
				"exec.builder.software: <software> parameter is not a software info. Provide the value obtained from assets.importSoftware() here"
			)

			soft = sw
			entrypointSet = true
			return self
		},






		arg: func(arg) {
			ll.assert(is_string(arg), "exec.arg(): argument must be a string, got: %v", arg)
			args = append(args, arg)
			argsTyped = append(argsTyped, {
				type: execConstants.ARG_TYPE_STRING,
				value: arg
			})
			return self
		},






		argWithVar: func(arg) {
			ll.assert(is_string(arg), "exec.argWithVar(): argument with var must be a string, got: %v", arg)


			t := feats.commandExpressions ? execConstants.ARG_TYPE_EXPRESSION : execConstants.ARG_TYPE_VAR
			argsTyped = append(argsTyped, {
				type: t,
				value: arg
			})
			typedArgSet = true

			return self
		},







		env: func(name, value) {
			validation.assertType([name, value], ["string"])
			envs[name] = value
			envsTyped[name] = {
				type: execConstants.ARG_TYPE_STRING,
				value: value
			}
			return self
		},







		envWithVar: func(name, value) {
			validation.assertType([name, value], ["string"])


			t := feats.commandExpressions ? execConstants.ARG_TYPE_EXPRESSION : execConstants.ARG_TYPE_VAR
			envsTyped[name] = {
				type: t,
				value: value
			}
			typedEnvSet = true

			return self
		},






		envMap: func(envMap) {
			enum.each(envMap, self.env)
			return self
		},







		secret: func(secretName, envName) {
			validation.assertType([secretName, envName], ["string"])
			secrets[envName] = "{secrets."+secretName+"}"
			return self
		},




		inHeavyQueue: func() {
			queue = execConstants.HEAVY_QUEUE
			return self
		},




		inMediumQueue: func() {
			queue = execConstants.MEDIUM_QUEUE
			return self
		},




		inLightQueue: func() {
			queue = execConstants.LIGHT_QUEUE
			return self
		},




		inUiQueue: func() {
			queue = execConstants.UI_TASKS_QUEUE
			return self
		},






		cpu: func(amount) {
			validation.assertType(amount, "number", "exec.builder.cpu: amount of cores must be a number")
			ll.assert(amount > 0, "exec.builder.cpu: amount of cores should be greater than 0")
			cpuRequest = amount

			return self
		},












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

			return self
		},







		cache: func(time) {
			ll.assert(
				is_int(time),
				"cache time must be an integer. " +
					"Did you forget to import a standard tengo library 'times'?")
			cache = time
			return self
		},






		cacheMillis: func(millis) {
			ll.assert(is_int(millis) && millis > 0, "cache time must be a number of milliseconds")
			cache = millis * times.millisecond
			return self
		},






		cacheSeconds: func(seconds) {
			ll.assert(is_int(seconds) && seconds > 0, "cache time must be a number of seconds")
			cache = seconds * times.second
			return self
		},






		cacheMinutes: func(minutes) {
			ll.assert(is_int(minutes) && minutes > 0, "cache time must be a number of minutes")
			cache = minutes * times.minute
			return self
		},






		cacheHours: func(hours) {
			ll.assert(is_int(hours) && hours > 0, "cache time must be a number of hours")
			cache = hours * times.hour
			return self
		},






		cacheDays: func(days) {
			ll.assert(is_int(days) && days > 0, "cache time must be a number of days")
			cache = days * times.hour * 24
			return self
		},






		nErrorLines: func(lines) {
			ll.assert(
				is_int(lines) && lines >= 0,
				"lines must be integer")

			nErrorLines = lines
			return self
		},









		addAsset: func(asset, destinationDir, ...pathsInArchive) {
			if (len(pathsInArchive) == 1 && is_array(pathsInArchive[0])) {

				pathsInArchive = pathsInArchive[0]
			}
			if len(pathsInArchive) == 0 {

				pathsInArchive = []
			}

			ll.assert(assets.isAssetInfo(asset),
				"exec.builder.addAsset: <asset> parameter is not an asset info. Provide the value obtained from assets.importAsset() here")
			validation.assertType(destinationDir, "string", "exec.builder.addAsset: <destinationDir> is not a string. It must be a path inside working directory")
			validation.assertType(pathsInArchive, ["string"], "exec.builder.addAsset: <pathsInArchive> must contain list of strings with paths inside archive")

			assetIdKey := ll.idToString(asset._id)
			if is_undefined(assetRules[assetIdKey]) {
				assetInfos[assetIdKey] = asset.resource
				assetDescriptors[assetIdKey] = ll.fromStrict(asset.descriptor)
				assetRules[assetIdKey] = {}
			}

			if len(pathsInArchive) == 0 || is_undefined(assetRules[assetIdKey][destinationDir]) {
				assetRules[assetIdKey][destinationDir] = []
			}

			if (len(pathsInArchive) > 0) {
				assetRules[assetIdKey][destinationDir] = append(assetRules[assetIdKey][destinationDir], pathsInArchive...)
			}

			return self
		},





		cacheAssetMillis: func (asset, millis) {
			ll.assert(assets.isAssetInfo(asset),
				"exec.builder.cacheAssetMillis: <asset> parameter is not an asset info. Provide the value obtained from assets.importAsset() here")
			ll.assert(is_int(millis),
				"exec.builder.cacheAssetMillis: asset cache time must be a number of milliseconds.")

			assetIdKey := ll.idToString(asset._id)
			assetCache[assetIdKey] = millis * times.millisecond

			return self
		},





		cacheAssetSeconds: func (asset, seconds) {
			ll.assert(assets.isAssetInfo(asset),
				"exec.builder.cacheAssetSeconds: <asset> parameter is not an asset info. Provide the value obtained from assets.importAsset() here")
			ll.assert(is_int(seconds),
				"exec.builder.cacheAssetSeconds: asset cache time must be a number of seconds.")

			assetIdKey := ll.idToString(asset._id)
			assetCache[assetIdKey] = seconds * times.second

			return self
		},





		cacheAssetMinutes: func (asset, minutes) {
			ll.assert(assets.isAssetInfo(asset),
				"exec.builder.cacheAssetMinutes: <asset> parameter is not an asset info. Provide the value obtained from assets.importAsset() here")
			ll.assert(is_int(minutes),
				"exec.builder.cacheAssetMinutes: asset cache time must be a number of minutes.")

			assetIdKey := ll.idToString(asset._id)
			assetCache[assetIdKey] = minutes * times.minute

			return self
		},





		cacheAssetHours: func (asset, hours) {
			ll.assert(assets.isAssetInfo(asset),
				"exec.builder.cacheAssetHours: <asset> parameter is not an asset info. Provide the value obtained from assets.importAsset() here")
			ll.assert(is_int(hours),
				"exec.builder.cacheAssetHours: asset cache time must be a number of hours.")

			assetIdKey := ll.idToString(asset._id)
			assetCache[assetIdKey] = hours * times.hour

			return self
		},





		cacheAssetDays: func (asset, days) {
			ll.assert(assets.isAssetInfo(asset),
				"exec.builder.cacheAssetHours: <asset> parameter is not an asset info. Provide the value obtained from assets.importAsset() here")
			ll.assert(is_int(days),
				"exec.builder.cacheAssetHours: asset cache time must be a number of days.")

			assetIdKey := ll.idToString(asset._id)
			assetCache[assetIdKey] = days * 24 * times.hour

			return self
		},

		enableMnz: func(productKey) {
			monetization.enable(mnz, productKey)

			monetization.appendRun(mnz)

			return self
		},

		setMnzUrl: func(url) {
			monetization.setUrl(mnz, url)
			return self
		},







		addFile: func(fileName, file, ...opts) {
			ll.assert(is_string(fileName), "exec.builder().addFile: fileName must be a string")
			ll.assert(smart.isReference(file), "exec.builder().addFile: file must be a reference")

			fileName = path.canonize(fileName)
			filesToAdd[fileName] = file
			monetization.addOptionalArg(mnz, fileName, opts...)
			return self
		},






		addFiles: func(filesMap, ...opts) {
			ll.assert(ll.isMap(filesMap), "exec.builder().addFiles: filesMap must be map of file names to file resources")
			for fileName, file in filesMap {
				self.addFile(fileName, file, opts...)
			}
			return self
		},








		writeFile: func(fileName, data, ...opts) {
			validation.assertType(data, ["or",
				"string",
				"bytes",
				validation.reference])

			fileName = path.canonize(fileName)

			filesToWrite[fileName] = data
			monetization.addOptionalArg(mnz, fileName, opts...)

			return self
		},






		mkDir: func(dir) {
			dir = path.canonize(dir)
			dirsToCreate = sets.add(dirsToCreate, dir)
			return self
		},






		saveFile: func(fileName) {
			fileName = path.canonize(fileName)
			sets.add(filesToSave, fileName)
			return self
		},






		saveFileContent: func(fileName) {
			fileName = path.canonize(fileName)
			sets.add(filesContentToSave, fileName)
			return self
		},







		saveFileSet: func(name, regex) {
			fileSetsToSave[name] = regex
			return self
		},




		processWorkdir: func(name, tpl, tplArgs) {
			wdProcessors[name] = {
				tpl: tpl,
				tplArgs: tplArgs
			}
			return self
		},







		saveFileSetContent: func(name, regex) {
			fileSetsContentToSave[name] = regex
			return self
		},






		streamFile: func(fileName) {
			fileName = path.canonize(fileName)
			sets.add(filesToStream, fileName)

			return self
		},




		stdoutFileName: func(fileName) {
			fileName = path.canonize(fileName)
			stdout = fileName
			return self
		},




		stderrFileName: func(fileName) {
			fileName = path.canonize(fileName)
			stderr = fileName
			return self
		},




		saveStdoutContent: func() {
			stdoutToValue = true
			return self
		},




		saveStderrContent: func() {
			stderrToValue = true
			return self
		},




		printErrStreamToStdout: func() {
			stderrToStdout = true
			return self
		},






		run: func() {
			if stderrToStdout {
				stderr = stdout
			}
			sets.add(filesToStream, stdout)
			sets.add(filesToStream, stderr)
			sets.add(filesToSave, stdout)
			sets.add(filesToSave, stderr)
			if stdoutToValue {
				sets.add(filesContentToSave, stdout)
			}
			if stderrToValue {
				sets.add(filesContentToSave, stderr)
			}




			filesToWriteRefs := maps.mapValues(filesToWrite, func(v) {
				if smart.isReference(v) { return v }
				return smart.createValueResource(constants.RTYPE_BINARY_VALUE, v)
			})

			ll.assert(entrypointSet,
				"incomplete command: cmd() or software() must be provided to exec.builder")

			runOptions := {
				cmd: cmd,
				args: args,
				envs: envs,
				secrets: secrets,
				queue: queue,
				stdout: stdout,
				stderr: stderr,
				nErrorLines: nErrorLines
			}

			pureExecInputs := {
				filesToAdd: smart.createMapResource(filesToAdd),
				filesToWrite: smart.createMapResource(filesToWriteRefs),
				dirsToCreate: slices.fromSet(dirsToCreate),
				runOptions: runOptions,
				filesToSave: slices.fromSet(filesToSave),
				filesContentToSave: slices.fromSet(filesContentToSave),
				fileSetsToSave: fileSetsToSave,
				fileSetsContentToSave: fileSetsContentToSave,
				filesToStream: slices.fromSet(filesToStream),
				wdProcessors: wdProcessors,
				assets: smart.createMapResource(assetInfos),
				assetDescriptors: assetDescriptors,
				assetRules: assetRules,
				assetCache: assetCache
			}

			serviceExecInputs := {}
			if feats.workdirLimits || feats.computeLimits {
				serviceExecInputs.quota = limits.quotaBuilder().
					queue(queue).
					cpu(cpuRequest). // 'undefined' value is OK and makes runner to apply default queue limits
					ram(ramRequest). // 'undefined' value is OK and makes runner to apply default queue limits
					build()
			}




			if monetization.shouldRun(mnz) {
				pureExecInputs.runOptions.monetization = monetization.toJson(mnz)
			}



			if typedArgSet || typedEnvSet {
				pureExecInputs.runOptions.argsTyped = argsTyped
				pureExecInputs.runOptions.envsTyped = envsTyped
			}

			if !is_undefined(soft) {
				pureExecInputs.softwareInfo = soft.resource
				pureExecInputs.runOptions.software = {
					name: soft.name,
					version: soft.version,
					descriptor: ll.fromStrict(soft.descriptor)
				}
			}

			tpl := render.create(assets.importTemplate("@platforma-sdk/workflow-tengo:exec.exec"), pureExecInputs, serviceExecInputs)

			self := undefined
			self = ll.toStrict(oop.inherit(tpl, {






				getFile: func(fileName) {
					return tpl.output("files", cache).getFutureInputField(fileName)
				},







				getFileContent: func(fileName) {
					return tpl.output("filesContent", cache).getFutureInputField(fileName)
				},







				getFileStream: func(fileName) {
					return tpl.output("fileStreams", cache).getFutureInputField(fileName, true)
				},







				getFileSet: func(fileSetName) {
					return tpl.output("fileSets", cache).getFutureInputField(fileSetName)
				},







				getFileSetContent: func(fileSetName) {
					return tpl.output("fileSetsContent", cache).getFutureInputField(fileSetName)
				},

				getProcessorResult: func(procName) {
					return tpl.output("wdProcessors", cache).getFutureInputField(procName)
				},




				getStdoutStream: func() {
					return self.getFileStream(stdout)
				},




				getStderrStream: func() {
					return self.getFileStream(stderr)
				},




				getStdoutFile: func() {
					return self.getFile(stdout)
				},




				getStderrFile: func() {
					return self.getFile(stderr)
				},




				getStdoutFileContent: func() {
					if stdoutToValue {
						return self.getFileContent(stdout)
					}
					ll.panic("stdout was not saved as a content")
				},




				getStderrFileContent: func() {
					if stderrToValue {
						return self.getFileContent(stderr)
					}
					ll.panic("stderr was not saved as a content")
				}
			}))

			return self
		}
	})

	return self
}

export ll.toStrict({
	builder: builder
})
