tx := import("tx")
ll := import("@platforma-sdk/workflow-tengo:ll")
render := import("@platforma-sdk/workflow-tengo:render")
smart := import("@platforma-sdk/workflow-tengo:smart")
sets := import("@platforma-sdk/workflow-tengo:sets")
maps := import("@platforma-sdk/workflow-tengo:maps")
tplutil := import("@platforma-sdk/workflow-tengo:tpl.util")
constants := import("@platforma-sdk/workflow-tengo:constants")
stateAwait := import("@platforma-sdk/workflow-tengo:tpl.state-await")
validation := import("@platforma-sdk/workflow-tengo:validation")










_unmarshal := func(resource, preprocessors) {
	ll.assert(smart.isResource(resource), "expected resource, found ", resource)


	self := func(resource) { return _unmarshal(resource, preprocessors) }

	for preproc in preprocessors {
		if preproc.canParse(resource) {
			return preproc.parse(resource, self)
		}
	}

	if smart.isReference(resource) && resource.hasError() {
		err := resource.getError().getDataAsJson()
		if !is_undefined(err.message) {
			err = err.message
		}
		ll.panic("input resource has errors: %v", err)
	}

	if smart.isResource(resource) && resource.checkResourceType(constants.RTYPE_NULL) {
		return undefined
	}

	if smart.isJsonResource(resource) || resource.checkResourceType(constants.RTYPE_BOBJECT_SPEC) {


		return resource.getDataAsJson()
	} else if smart.isMapResource(resource) || smart.isLockedEphMapResource(resource) {


		result := {}
		for field, value in resource.inputs() {
			if value.isErrorSet() {
				ll.panic("input %v field has error: %v", field, field.getError().getDataAsJson().message)
			} else if value.isSet() {
				result[field] = _unmarshal(value.getValue(), preprocessors)
			} else {
				result[field] = value
			}

		}
		return result
	} else {

		return resource
	}
}









tpl := func(ops) {
	self := undefined

	_renderer := smart.resourceBuilder(ll.getCurrentTemplateRenderer())

	_rendererType :=  _renderer.info().Type

	_isEphemeral := _rendererType.Name == constants.RTYPE_RENDER_EPH_TEMPLATE.Name

	_hasPrepared := _renderer.get(tplutil.INPUTS_MAP_FIELD).hasField(tplutil.PREPARE_MAP_FIELD_NAME)

	_preparedMap := _hasPrepared ? _renderer.get(tplutil.INPUTS_MAP_FIELD).get(tplutil.PREPARE_MAP_FIELD_NAME) : undefined

	_templateId := _renderer.get(tplutil.TEMPLATE_FIELD).id

	_stateRequests := []
	_delegatedOutputs := []

	self = {




		renderer: func() {
			return _renderer
		},




		isEphemeral: func() {
			return _isEphemeral
		},




		template: func() {
			return smart.resource(_templateId)
		},

		_unmarshallers: copy(ops.unmarshallers),






		registerUnmarshaller: func(preproc) {
			ll.assert(is_undefined(self._inputs), "input preprocessors must be registered before getting inputs")
			self._unmarshallers = append(self._unmarshallers, preproc)
		},




		rawInputs: func() {
			return self.renderer().get(tplutil.INPUTS_MAP_FIELD).inputs()
		},

		_inputs: undefined,




		inputs: func() {
			if self._inputs == undefined {
				inputs := _unmarshal(self.renderer().get(tplutil.INPUTS_MAP_FIELD), self._unmarshallers)


				pp := inputs[tplutil.PREPARE_MAP_FIELD_NAME]
				if !is_undefined(pp) {
					delete(inputs, tplutil.PREPARE_MAP_FIELD_NAME)
					for k, v in pp {
						ll.assert(is_undefined(inputs[k]), k, " key already exists in inputs map")
						inputs[k] = v
					}
				}

				self._inputs = inputs
			}
			return self._inputs
		},




		hasInput: func(name) {
			return self.renderer().get(tplutil.INPUTS_MAP_FIELD).hasField(name)
		},


		definedOutputs : [],







		defineOutputs : func(...name) {
			if len(name) == 1 && is_array(name[0]) {
				for v in name[0] {
					self.definedOutputs = append(self.definedOutputs, v)
				}
			} else {
				self.definedOutputs = append(self.definedOutputs, name...)
			}
		},


































		awaitState: func(...pathAndState) {
			if !self.isEphemeral() {
				ll.panic("awaitState allowed only in ephemeral templates")
			}
			self.awaitStateIfEphemeral(pathAndState...)
		},


































		awaitStateIfEphemeral: func(...pathAndState) {
			if !self.isEphemeral() {
				return
			}
			path := pathAndState[:len(pathAndState) - 1]
			state := pathAndState[len(pathAndState) - 1]
			_stateRequests = append(_stateRequests, {
				path: path,
				state: state
			})
		},





		ignorePrepare: _hasPrepared,




		preparedMapResource: func() {
			return _preparedMap
		},




		prepareQueue : [],







		prepare : func(codeFn) {
			if self.ignorePrepare {
				return
			}

			ll.assert(len(self.prepareQueue) == 0, "multiple prepare statements are not supported yet")

			self.prepareQueue = append(self.prepareQueue, codeFn)
		},





		renderPrepare: func() {
			ll.assert(!self.ignorePrepare, "expected !ignorePrepare")

			inputs := self.inputs()


			newInputs := copy(inputs)


			builder := undefined
			if self.isEphemeral() {
				builder = smart.ephemeralMapBuilder()
			} else {
				builder = smart.mapBuilder()
			}


			self._validateInputs()
			for cb in self.prepareQueue {

				r := cb(inputs)

				ll.assert(is_map(r), "expected map, got ", r)

				for name, ref in r {
					ll.assert(is_undefined(newInputs[name]), "prepare name is already in use: %s", name)

					builder.add(name, ref)
				}
			}
			newInputs[tplutil.PREPARE_MAP_FIELD_NAME] = builder.build()


			renderer := undefined
			if self.isEphemeral() {
				renderer = render.createEphemeral(self.template(), newInputs)
			} else {
				renderer = render.create(self.template(), newInputs)
			}

			for output in self.definedOutputs {
				self.getOutputField(output).set(renderer.output(output))
			}
		},





		setOutputs : func(resultMap) {
			ll.assert(ll.isMap(resultMap), "expected map, got ", resultMap)


			for name, value in resultMap {
				f := self.getOutputField(name)
				if !f.isSet() {
					f.setRefOrJson(value)
				}
			}
		},




		getOutputField: func(name) {
			return self.renderer().getField(tplutil.TEMPLATE_OUTPUT_PREFIX + name)
		},




		createOutputsAndLock: func(outputs) {
			for name in outputs {
				self.renderer().createOutputField(tplutil.TEMPLATE_OUTPUT_PREFIX + name)
			}
			self.renderer().lockOutputs()
		},

		_inputsSchema: undefined,




		validateInputs: func(schema) {
			ll.assert(is_undefined(self._inputsSchema), "validateInputs was already invoked")
			self._inputsSchema  = schema
		},

		_validateInputs: func() {
			schema := self._inputsSchema
			if is_undefined(schema) {
				return
			}
			validation.assertType(self.inputs(), schema)
		},




		body: func(bodyFn){
			if self.isEphemeral() {

				self.routeEph(bodyFn)
			} else {

				self.routePure(bodyFn)
			}
		},




		routePure: func(bodyFn){

			if len(self.definedOutputs) == 0 {
				ll.panic("please define outputs before template body")
			}













			if ll.isInitializing() {


				self.createOutputsAndLock(self.definedOutputs)
			} else {


				if len(self.prepareQueue) != 0 {


					self.renderPrepare()
				} else {

					self._validateInputs()
					result := bodyFn(self.inputs())

					ll.assert(ll.isMap(result),
						"expected map from tpl.body() callback. Did you forget 'return' statement?")

					outputsWithValues := sets.fromMapKeys(result)
					outputsWithValues = sets.add(outputsWithValues, _delegatedOutputs...)

					ll.assert(sets.fromSlice(self.definedOutputs) == outputsWithValues,
						"tpl.body() callback must return the same list of outputs as were given to 'defineOutputs()'. Want %s, got %s",
						self.definedOutputs, sets.toSlice(outputsWithValues))


					self.setOutputs(result)
				}
			}
		},




		routeEph: func(bodyFn) {

			if len(_stateRequests) == 0 {
				_stateRequests = [{
					path: [],
					state: "AllInputsSet"
				}]
			}

			if _hasPrepared {
				_stateRequests = append(_stateRequests, {
					path: [tplutil.PREPARE_MAP_FIELD_NAME],
					state: "ResourceReady"
				})
			}

			stateReached := stateAwait.await(
				_renderer.getField(tplutil.INPUTS_MAP_FIELD),
				_renderer,
				_stateRequests
			)

			if stateReached {

				if len(self.prepareQueue) > 0 {
					ll.assert(len(self.definedOutputs) > 0, "outputs should be defined prior to prepare")

					self.createOutputsAndLock(self.definedOutputs)

					self.renderPrepare()
				} else {


					self._validateInputs()
					result := bodyFn(self.inputs())
					ll.assert(ll.isMap(result),
						"expected map from tpl-eph.body() callback. Did you forget 'return' statement?")

					self.createOutputsAndLock(maps.getKeys(result))


					self.setOutputs(result)
				}
			}
		},









		routeDelegate: func(tpl, extraInputs, outputs) {
			_delegatedOutputs = outputs

			inputs := self.rawInputs()
			for k, v in extraInputs {
				inputs[k] = v
			}

			renderer := undefined
			if self.isEphemeral() {
				renderer = render.createEphemeral(tpl, inputs)
			} else {
				renderer = render.create(tpl, inputs)
			}

			tplOutputs := {}
			for o in outputs {
				tplOutputs[o] = renderer.output(o)
			}

			self.createOutputsAndLock(outputs)
			self.setOutputs(tplOutputs)
		},






		api: func() {
			return ll.toStrict({
				renderer                : self.renderer,
				registerUnmarshaller    : self.registerUnmarshaller,
				rawInputs               : self.rawInputs,
				hasInput                : self.hasInput,
				inputs                  : self.inputs,
				defineOutputs           : self.defineOutputs,
				prepare                 : self.prepare,
				awaitState              : self.awaitState,
				awaitStateIfEphemeral   : self.awaitStateIfEphemeral,
				delegate                : self.routeDelegate,
				validateInputs          : self.validateInputs,
				body                    : self.body,
				template                : self.template
			})
		}
	}
	return self
}

export ll.toStrict({
	tpl: tpl
})
