ll := import("@platforma-sdk/workflow-tengo:ll")
smart := import("@platforma-sdk/workflow-tengo:smart")
maps := import("@platforma-sdk/workflow-tengo:maps")
constants := import("@platforma-sdk/workflow-tengo:constants")
json := import("json")
assets := import("@platforma-sdk/workflow-tengo:assets")
render := import("@platforma-sdk/workflow-tengo:render")
validation := import("@platforma-sdk/workflow-tengo:validation")
model := import("@platforma-sdk/workflow-tengo:workflow.model")

queryAnchoredTpl := assets.importTemplate("@platforma-sdk/workflow-tengo:workflow.query-anchored")

_CONTEXT_FIELD_NAME := "ctx"


_OPTIONS_SCHEME := {
	"spec,?": `bool`,
	"data,?": `bool`,
	"single,?": `bool`,
	"first,?": `bool`,
	"errIfMissing,?": `bool`
}


_DEFAULT_OPTIONS := {

	spec: true,

	data: true,

	single: false,

	first: false,

	errIfMissing: false
}


_QUERY_MAP_SCHEMA := {
	`__options__,closed`: true,
	`any`: ["or",
		model.AP_COLUMN_SELECTOR_SCHEMA,
		{
			`__options__,closed`: true,
			`query`: model.AP_COLUMN_SELECTOR_SCHEMA,
			`specOnly`: `bool`
		}
	]
}


































































create := func(spec, ctx, ...options) {



	ops := copy(_DEFAULT_OPTIONS)
	if len(options) > 0 {
		ll.assert(len(options) == 1, "expected map, got array: ", options)




		for k, v in options[0] {
			ops[k] = v
		}
	}

	query := json.encode({
		query: spec,
		setErrorIfMissing: ops.errIfMissing
	})

	builder := undefined
	if ops.first {
		builder = smart.ephemeralBuilder(constants.RTYPE_BRESOLVE_FIRST, query)
	} else {

		builder = smart.ephemeralBuilder(constants.RTYPE_BRESOLVE_CHOICE, query)
	}

	builder.createInputField(_CONTEXT_FIELD_NAME).set(ctx)
	resolve := builder.lockInputsAndBuild()




	rb := smart.structBuilder(constants.RTYPE_QUERY_RESULT)


	rb.createInputField("ops").setJson(ops)

	if ops.spec {
		rb.createInputField("spec").set(resolve.getFutureOutputField("spec"))
	}


	if ops.data {
		rb.createInputField("data").set(resolve.getFutureOutputField("data"))
	}



	if ops.first {
		rb.createInputField("ref").set(resolve.getFutureOutputField("address"))
	}

	return rb.lockAndBuild()
}




resultUnmarshaller := func() {
	return  {
		canParse: func(r) {
			return smart.isResource(r) && r.info().Type.Name == constants.RTYPE_QUERY_RESULT.Name
		},

		parse: func(resource, parser)  {

			ops := resource.get("ops").getDataAsJson()





			getData := func(res, fieldName) {
				field := res.getField(fieldName)
				if field.isSet() {
					return parser(field.getValue())
				} else {
					return field
				}
			}

			if ops.first {



				isNR := func(r) {
					return r.info().Type.Name == constants.RTYPE_BRESOLVE_NO_RESULT.Name
				}

				if isNR(resource.get("ref")) {
					return undefined
				}

				result := {
					ref: resource.get("ref").getDataAsJson(),
					spec: resource.get("spec").getDataAsJson()
				}

				if resource.hasInputField("data") {
					result.data = getData(resource, "data")
				}

				return ll.toStrict(result)
			} else {


				specs := resource.get("spec").inputs() // map of specs
				datas := undefined
				if resource.hasInputField("data") {
					datas = resource.get("data") // map of datas
				}

				result := []
				maps.forEach(specs, func(ref, s) {
					r := {
						ref: json.decode(ref),
						spec: s.getValueAsJson()
					}


					if !is_undefined(datas) {
						r.data = getData(datas, ref)
					}

					result = append(result, ll.toStrict(r))
				})

				if ops.single {
					ll.assert(len(result) == 1, "expected single element, got ", len(result))

					result = result[0]
				}

				return result
			}
		}
	}
}








resolve := func(ref, ctx, ...options) {

	spec := {
		type: "Direct",
		block_id: ref.blockId, 	// @TODO use consistent naming in pl
		output_name: ref.name 	// @TODO use consistent naming in pl
	}

	ops := {}

	if len(options) > 0 {
		ops = options[0]
	}

	ops.first = true

	return create(spec, ctx, ops)
}


_ANCHORED_QUERY_DEFAULT_OPTIONS := {

	ignoreMissingDomains: false
}
























anchoredQuery := func(ctx, anchors, queryMap, ...opts) {

	validation.assertType(queryMap, _QUERY_MAP_SCHEMA, "invalid query map schema")


	normalizedOpts := _ANCHORED_QUERY_DEFAULT_OPTIONS
	if len(opts) > 0 {
		ll.assert(len(opts) == 1 && is_map(opts[0]), "expected single map for opts, got: ", opts)
		normalizedOpts = maps.deepMerge(normalizedOpts, opts[0])
	}


	resolvedAnchors := {}
	for anchorId, anchorValue in anchors {
		opts := { spec: true, data: true }

		ref := anchorValue
		if !is_undefined(anchorValue.ref) {
			ref = anchorValue.ref
			if anchorValue.specOnly {
				opts.data = false
			}
		}

		resolvedAnchors[anchorId] = resolve(ref, ctx, opts)
	}


	processedQueryMap := {}
	for queryId, aQuery in queryMap {
		query := aQuery
		fetchData := true

		if !is_undefined(aQuery.query) {
			query = aQuery.query
			if aQuery.specOnly {
				fetchData = false
			}
		}

		processedQueryMap[queryId] = {
			query: query,
			fetchData: fetchData
		}
	}


	queryResult := render.createEphemeral(queryAnchoredTpl, {
		ctx: ctx,
		anchorSpecs: smart.createMapResource(maps.mapValues(resolvedAnchors, func(v) { return v.inputs().spec })),
		request: {
			queryMap: processedQueryMap,
			opts: normalizedOpts
		}
	})


	self := undefined
	self = ll.toStrict({





		getAnchor: func(anchorId) {
			anchor := resolvedAnchors[anchorId]
			if is_undefined(anchor) {
				ll.panic("unknown anchor ID: " + anchorId)
			}
			return anchor
		},






		getResult: func(queryId) {
			return queryResult.output("result/" + queryId)
		},






		getResultSpec: func(queryId) {
			return self.getResult(queryId).getFutureInputField("spec")
		},






		getResultData: func(queryId) {
			return self.getResult(queryId).getFutureInputField("data")
		},






		getResultRef: func(queryId) {
			return self.getResult(queryId).getFutureInputField("ref")
		}
	})

	return self
}

export ll.toStrict({
	create: create,
	resolve: resolve,
	resultUnmarshaller: resultUnmarshaller,
	anchoredQuery: anchoredQuery
})
