






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

json := import("json")
times := import("times")
text := import("text")
oop := import("@platforma-sdk/workflow-tengo:oop")
maps := import("@platforma-sdk/workflow-tengo:maps")
path := import("@platforma-sdk/workflow-tengo:path")
pkg := import("@platforma-sdk/workflow-tengo:exec.package")
runcmd := import("@platforma-sdk/workflow-tengo:exec.runcmd")
python := import("@platforma-sdk/workflow-tengo:exec.python")
R := import("@platforma-sdk/workflow-tengo:exec.R")
constants := import("@platforma-sdk/workflow-tengo:exec.constants")
desc := import("@platforma-sdk/workflow-tengo:exec.descriptor")
validation := import("@platforma-sdk/workflow-tengo:validation")


_ARG_VAR_PKG            := "pkg"  // abs path to software package
_ARG_VAR_JAVA           := "java" // abs path to 'java' run environment
_ARG_VAR_PYTHON         := "python" // abs path to 'python' run environment
_ARG_VAR_R              := "R" // abs path to 'R' run environment runtime
_ARG_VAR_R_VENV         := "rVenv" // abs path to 'R' portable virtual environment
_ARG_VAR_MNZ            := "monetization" // monetization env value


_expr := func(varName) {
	return "{" + varName + "}"
}

_newTypedArg := func(type, value) {
	return ll.toStrict({
		type: type,
		value: value
	})
}








_newArg := func(isString, value) {
	if isString {
		return _newTypedArg(constants.ARG_TYPE_STRING, value)
	}
	if feats.commandExpressions {
		return _newTypedArg(constants.ARG_TYPE_EXPRESSION, value)
	}
	return _newTypedArg(constants.ARG_TYPE_VAR, value)
}

_newRefVar := func(varName, refName, reference) {

	if !feats.commandExpressions {
		return ll.toStrict({
			varName: "{" + varName + "}",
			refName: refName,
			reference: reference
		})
	}

	return ll.toStrict({
		varName: varName,
		refName: refName,
		reference: reference
	})
}

_installSoftware := func(registry, pkgName) {
	ll.assert(is_string(registry), "exec.internal._installSoftware(): registry is not a string, got: %v", registry, pkgName)
	ll.assert(is_string(pkgName), "exec.internal._installSoftware(): pkgName is not a string, got: %v", pkgName, registry)

	archive := pkg.get(registry, pkgName).archive()
	return pkg.install(archive).package()
}

_directCallOptions := func(runOptions) {
	return ll.toStrict({
		variables: [],
		customPaths: [],
		cmd: _newArg(true, runOptions.cmd),
		args: [],
		envs: {}
	})
}

_loadJavaRunEnvironment := func(operationMode, descriptor) {
	javaPkg := _installSoftware(descriptor.runEnv.registry, descriptor.runEnv.package)
	javaRefVar := _newRefVar(_ARG_VAR_JAVA, descriptor.runEnv.name, javaPkg)

	return ll.toStrict({
		runEnvType: "java",

		variables: [ javaRefVar ],
		customPaths: [ _newArg(false, path.join(_expr(_ARG_VAR_JAVA), descriptor.runEnv.binDir)) ],
		envs: {}
	})
}

_loadPythonRunEnvironment := func(operationMode, descriptor, softName, softPkg) {
	venvBuilder := python.venvBuilder().
		software(softPkg).
		runEnvDescriptor(descriptor.runEnv).
		dependencies(descriptor.dependencies)

	if !is_undefined(descriptor.toolset) && descriptor.toolset != "" {
		venvBuilder.useToolset(descriptor.toolset)
	}

	venv := venvBuilder.
		cacheDays(7).
		build().
		venv()

	venvRoot := path.join(_expr(_ARG_VAR_PYTHON), "venv")
	venvRefKey := softName + "/venv"

	pythonRefVar := _newRefVar(_ARG_VAR_PYTHON, venvRefKey, venv)

	return ll.toStrict({
		runEnvType: "python",

		variables: [ pythonRefVar ],
		customPaths: [ _newArg(false, path.join(venvRoot, "bin")) ],
		envs: {
			"VIRTUAL_ENV": _newArg(false, venvRoot)
		}
	})
}

_loadRRunEnvironment := func(operationMode, descriptor, softName, softPkg) {
	renvBuilder := R.rVenvBuilder().
		operationMode(operationMode).
		software(softPkg).
		runEnvDescriptor(descriptor.runEnv).
		dependencies(descriptor.dependencies)

	if !is_undefined(descriptor.toolset) && descriptor.toolset != "" {
		renvBuilder.useToolset(descriptor.toolset)
	}

	rRuntime := renvBuilder.
		cacheDays(7).
		build()

	rEnv := rRuntime.venv()
	rPackage := rRuntime.R()

	rBinDir := path.join(_expr(_ARG_VAR_R), descriptor.runEnv.binDir)

	rRefKey := softName + "/R-runtime"
	rRefVar := _newRefVar(_ARG_VAR_R, rRefKey, rPackage)

	rEnvRefKey := softName + "/R-virtual-env"
	rEnvRefVar := _newRefVar(_ARG_VAR_R_VENV, rEnvRefKey, rEnv)

	renvRootArg := path.join(_expr(_ARG_VAR_R), "renv-root")

	envs := {
		"RHOME": _newArg(false, _expr(_ARG_VAR_R)), // for Rscript (it uses RHOME instead of R_HOME)
		"R_HOME_DIR": _newArg(false, _expr(_ARG_VAR_R)), // for R startup script. It configures all the rest required for R to start.
		"RENV_PATHS_ROOT": _newArg(false, renvRootArg),
		"RENV_PATHS_BINARY": _newArg(false, path.join(renvRootArg, "binaries")),
		"RENV_PATHS_SOURCE": _newArg(false, path.join(renvRootArg, "sources")),
		"RENV_PATHS_CACHE": _newArg(false, path.join(renvRootArg, "cache")),
		"RENV_PATHS_RENV": _newArg(false, path.join(_expr(_ARG_VAR_R_VENV), "renv")),
		"RENV_PATHS_LOCKFILE": _newArg(false, path.join(renvRootArg, "renv.lock"))
	}




	envs["RENV_CONFIG_REPOS_OVERRIDE"] = _newArg(false, path.join(_expr(_ARG_VAR_R), "packages"))

	return ll.toStrict({
		runEnvType: "R",

		variables: [ rRefVar, rEnvRefVar ],
		customPaths: [ _newArg(false, rBinDir) ],
		envs: envs
	})
}


_loadRunEnvironment := func(operationMode, descriptor, softName, softPackage) {
	if descriptor.runEnv.type == "java" {
		return _loadJavaRunEnvironment(operationMode, descriptor)
	}
	if descriptor.runEnv.type == "python" {
		return _loadPythonRunEnvironment(operationMode, descriptor, softName, softPackage)
	}
	if descriptor.runEnv.type == "R" {
		return _loadRRunEnvironment(operationMode, descriptor, softName, softPackage)
	}

	ll.panic("run environment type %q is not currently supported by workflow-sdk",	descriptor.runEnv.type)
}




_binPackageCallOptions := func(operationMode, softwareName, variables, descriptor, softPackage) {
	customPaths := []
	envs := ll.toStrict({})
	args := []
	runEnvType := "binary"



	if (maps.containsKey(descriptor, "runEnv")) {
		runEnvInfo := _loadRunEnvironment(operationMode, descriptor, softwareName, softPackage)

		runEnvType = runEnvInfo.runEnvType
		if len(runEnvInfo.variables) > 0 {
			variables = append(variables, runEnvInfo.variables...)
		}
		if len(runEnvInfo.customPaths) > 0 {
			customPaths = append(customPaths, runEnvInfo.customPaths...)
		}
		if len(runEnvInfo.envs) > 0 {
			for n, v in runEnvInfo.envs {
				envs[n] = v
			}
		}
	}

	ll.assert(
		len(descriptor.cmd) != 0,
		"cannot render final command for software: empty 'cmd' and 'command' in software descriptor")

	cmd := _newArg(false, descriptor.cmd[0])

	for arg in splice(descriptor.cmd, 1) {
		args = append(args, _newArg(false, arg))
	}

	return ll.toStrict({
		runEnvType: runEnvType,
		variables: variables,
		customPaths: customPaths,
		cmd: cmd,
		args: args,
		envs: envs
	})
}

_devLocalPackageCallOptions := func(softwareName, localDescriptor) {
	softPackage := pkg.use({
		hash: localDescriptor.hash,
		path: localDescriptor.path,
		descriptor: json.encode(ll.fromStrict(localDescriptor)),
		origin: {
			registry: "dev-local",
			url: "file://" + localDescriptor.path,
			packageName: softwareName
		}
	}).package()

	variables := [ _newRefVar(_ARG_VAR_PKG, softwareName, softPackage) ]

	return _binPackageCallOptions("dev-local", softwareName, variables, localDescriptor, softPackage)
}

_remotePackageCallOptions := func(softwareName, binaryDescriptor) {
	softPackage := _installSoftware(binaryDescriptor.registry, binaryDescriptor.package)

	variables := [ _newRefVar(_ARG_VAR_PKG, softwareName, softPackage) ]

	return _binPackageCallOptions("remote", softwareName, variables, binaryDescriptor, softPackage)
}





swToRunCmdOptions := func(softwareName, softwareVersion, softwareDescriptor) {
	ll.assert(is_string(softwareName),
		"exec.internal.swToRunCmdOptions: software name must be a string, got %v", softwareName)
	ll.assert(is_string(softwareVersion),
		"exec.internal.swToRunCmdOptions: software version must be a string, got %v", softwareVersion)
	validation.assertType(softwareDescriptor, desc.scheme,
		"exec.internal.swToRunCmdOptions: descriptor does not fit schema")

	result := undefined
	if maps.containsKey(softwareDescriptor, "local") {
		result = _devLocalPackageCallOptions(softwareName, softwareDescriptor.local)
	} else if maps.containsKey(softwareDescriptor, "binary") {
		result = _remotePackageCallOptions(softwareName, softwareDescriptor.binary)
	} else {
		ll.panic(
			"Unable to run software %s v%s: 'binary' execution mode is not enabled in software descriptor",
			softwareName, softwareVersion)
	}



	return ll.toStrict({
		runEnvType: result.runEnvType,
		variables: result.variables,
		customPaths: result.customPaths,
		cmd: result.cmd,
		args: result.args,
		envs: result.envs
	})
}

binaryRunOptions := func(runOptions, mnzEnvs) {
	validation.assertType(mnzEnvs, {"any": validation.reference})

	runCmdOpts := undefined
	if (is_undefined(ll.fromStrict(runOptions).software)) {



		runCmdOpts = _directCallOptions(runOptions)
	} else {
		software := runOptions.software
		descriptor := software.descriptor

		runCmdOpts = swToRunCmdOptions(software.name, software.version, descriptor)
	}





	opts := ll.fromStrict(runOptions)
	shouldUseNewTyped := !is_undefined(opts.argsTyped) || !is_undefined(opts.envsTyped)

	args := shouldUseNewTyped ? runOptions.argsTyped : runOptions.args
	for _, arg in args {
		typedArg := shouldUseNewTyped ? _newTypedArg(arg.type, arg.value) :	_newArg(true, arg)
		runCmdOpts.args = append(runCmdOpts.args, typedArg)
	}

	envs := shouldUseNewTyped ? runOptions.envsTyped : runOptions.envs
	for name, v in envs {
		typedEnv := shouldUseNewTyped ? _newTypedArg(v.type, v.value) : _newArg(true, v)
		runCmdOpts.envs[name] = typedEnv
	}

	for name, val in mnzEnvs { // can be empty


		runCmdOpts.envs[name] = _newArg(false, _expr(_ARG_VAR_MNZ))

		runCmdOpts.variables = append(runCmdOpts.variables,
			_newRefVar(_ARG_VAR_MNZ, "monetization", val))
	}


	return oop.inherit(runOptions, ll.fromStrict(runCmdOpts))
}

prepareCommandRun := func(wd, opts) {
	runBuilder := runcmd.builder(wd)


	for varInfo in opts.variables {
		runBuilder.ref(varInfo.refName, varInfo.reference)
		runBuilder.substitutionRule(varInfo.varName, varInfo.refName)
	}


	for p in opts.customPaths {
		runBuilder.addToPath(p.type, p.value)
	}


	for name, value in opts.envs {
		runBuilder.envTyped(name, value.type, value.value)
	}

	return runBuilder
}

_activateRenv := func(wd, opts) {
	activateRenv := prepareCommandRun(wd, opts)
	activateRenv.
		cmd("R").
		arg("-e").arg("renv::activate()")

	activateRenvRun := activateRenv.build()

	return activateRenvRun
}

prepareWDForRunEnvironment := func(wd, opts) {
	if (!maps.containsKey(opts, "runEnvType")) {
		return wd
	}

	if (opts.runEnvType == "R") {
		return _activateRenv(wd, opts).workdir
	}

	return wd
}

export {
	swToRunCmdOptions           : swToRunCmdOptions,
	binaryRunOptions			: binaryRunOptions,

	prepareWDForRunEnvironment	: prepareWDForRunEnvironment,
	prepareCommandRun			: prepareCommandRun
}
