





ll := import("@platforma-sdk/workflow-tengo:ll")
maps := import("@platforma-sdk/workflow-tengo:maps")
sets := import("@platforma-sdk/workflow-tengo:sets")
slices := import("@platforma-sdk/workflow-tengo:slices")
validation := import("@platforma-sdk/workflow-tengo:validation")
json := import("json")
canonical := import("@platforma-sdk/workflow-tengo:canonical")





P_UNTYPED_SPEC_SCHEMA := {
	`name`: `string`,
	`domain,?`: { any: `string` },
	`contextDomain,?`: { any: `string` },
	`annotations,?`: { any: `string` },
	`parentAxes,?`: [ `number` ]
}

P_AXIS_SPEC_SCHEMA := ["and", P_UNTYPED_SPEC_SCHEMA, {
	`type`: `string,regex=Int|Long|String`
}]

P_COLUMN_OWN_SPEC_SCHEMA := ["and", P_UNTYPED_SPEC_SCHEMA, {
	`valueType`: `string,regex=Int|Long|Float|Double|String`
}]

P_COLUMN_SPEC_SCHEMA := ["and", P_UNTYPED_SPEC_SCHEMA, {
	`kind`: `string,regex=PColumn`,
	`valueType`: `string`,
	`axesSpec,?`: [ P_AXIS_SPEC_SCHEMA ]
}]

P_COLUMN_STRICT_SPEC_SCHEMA := ["and", P_COLUMN_SPEC_SCHEMA, {
	`valueType`: `string,regex=Int|Long|Float|Double|String`
}]

P_OBJECT_SPEC_SCHEMA := ["or", P_COLUMN_SPEC_SCHEMA, {
	`kind`: `string`,
	`annotations,?`: { any: `string` }
}]

KIND_P_COLUMN := "PColumn"






N_LABEL := "pl7.app/label"






D_BLOCK := "pl7.app/block"






A_LABEL := "pl7.app/label"

A_IS_LINKER_COLUMN := "pl7.app/isLinkerColumn"











A_TRACE := "pl7.app/trace"





normalizeColumnSpec := func(spec) {
	if is_undefined(spec.axesSpec) {
		spec.axesSpec = []
	}
	validation.assertType(spec, P_COLUMN_SPEC_SCHEMA)
	return spec
}





_P_MATCHER_SCHEMA := {

	`type,?`: `string,regex=Int|Long|Float|Double|String`,
	`name`: `string`,
	`domain,?`: { any: `string` },
	`contextDomain,?`: { any: `string` },
	`optional,?`: `boolean`
}

_normalizeMatcher := func(matcher) {
	if is_int(matcher) {
		return matcher
	} else if is_string(matcher) {
		return { name: matcher }
	} else {
		validation.assertType(matcher, _P_MATCHER_SCHEMA)
		return matcher
	}
}

_checkAxisMatch := func(axisSpec, matcher) {
	if axisSpec.name != matcher.name {
		return false
	}
	if !is_undefined(matcher.type) && axisSpec.type != matcher.type {
		return false
	}
	if !is_undefined(matcher.domain) {
		for domain, domainValue in matcher.domain {
			if is_undefined(axisSpec.domain) || axisSpec.domain[domain] != domainValue {
				return false
			}
		}
	}
	if !is_undefined(matcher.contextDomain) {
		for key, value in matcher.contextDomain {
			if is_undefined(axisSpec.contextDomain) || axisSpec.contextDomain[key] != value {
				return false
			}
		}
	}
	return true
}






matchAxis := func(axesSpec, matcher) {
	validation.assertType(axesSpec, [ P_AXIS_SPEC_SCHEMA ])
	matcher = _normalizeMatcher(matcher)
	found := -1
	for idx, axisSpec in axesSpec {
		if is_int(matcher) {
			if idx != matcher {
				continue
			}
		} else if !_checkAxisMatch(axisSpec, matcher) {
			continue
		}
		if found != -1 {
			ll.panic("multiple matches found for %v in %v", matcher, axesSpec)
		}
		found = idx
	}
	if found == -1 && !matcher.optional {
		ll.panic("match not found for %v in %v", matcher, axesSpec)
	}
	return found
}





























matchAxes := func(axesSpec, matchers, ...opts) {
	keepUnmatched := false
	if len(opts) == 1 {
		keepUnmatched = opts[0].keepUnmatched
	} else if len(opts) != 0 {
		ll.panic("wrong number of parameters for matchAxes")
	}

	result := []
	resultSet := {}
	for matcher in matchers {
		idx := matchAxis(axesSpec, matcher)
		if idx != -1 {
			idxS := string(idx)
			if resultSet[idxS] {
				ll.panic("duplicate match detecterd for %v in %v (idx = %v)", matchers, axesSpec, idx)
			}
			resultSet[idxS] = true
		}
		if idx != -1 || keepUnmatched {
			result = append(result, idx)
		}
	}
	return result
}





_TRACE_STEP_SCHEMA := {
	`type`: `string`,
	`label`: `string`,
	`importance,?`: `number`,
	`id,?`: `string`,
	`__options__,closed`: ``
}

_TRACE_SCHEMA := [_TRACE_STEP_SCHEMA]







makeTrace := func(...steps) {
	ll.assert(len(steps) > 0, "makeTrace: expects at least one argument")
	

	input := steps[0]
	steps = steps[1:]
	currentTrace := []
	
	if (!validation.isValid(input, _TRACE_STEP_SCHEMA)) {
		inputTraceContent := undefined

		if is_string(input) {
			inputTraceContent = input
		} else if is_map(input) && is_string(input[A_TRACE]) {
			inputTraceContent = input[A_TRACE]
		} else if is_map(input["annotations"]) && is_map(input["annotations"]) && is_string(input["annotations"][A_TRACE]) {
			inputTraceContent = input["annotations"][A_TRACE]
		}

		if !is_undefined(inputTraceContent) {
			currentTrace = json.decode(inputTraceContent)
			validation.assertType(currentTrace, _TRACE_SCHEMA)
		}
	}

	if len(steps) > 0 {
		validation.assertType(steps, _TRACE_SCHEMA)
		currentTrace = append(currentTrace, steps...)
	}
	currentTraceStr := string(json.encode(currentTrace))

	return ll.toStrict({
		value: currentTrace,
		valueStr: currentTraceStr,












		inject: func(spec, ...opts) {
			validation.assertType(spec, P_COLUMN_SPEC_SCHEMA)


			options := {}
			if len(opts) > 0 {
				if len(opts) == 1 && is_map(opts[0]) {
					options = opts[0]
				} else {
					ll.panic("inject: optional parameters must be a single map argument, got: %v", opts)
				}
			}


			override := options.override
			if is_undefined(override) {
				override = true
			}

			return maps.deepTransform(spec, {annotations: {"pl7.app/trace": func(oldValue) {

				if override || is_undefined(oldValue) {
					return currentTraceStr
				}
				return oldValue
			}}})
		}
	})
}








createSpecDistiller := func(specs) {
	ll.assert(is_array(specs), "specs must be an array")


	domainsValues := {}
	contextDomainsValues := {}

	addDomainValue := func(valuesMap, name, domainName, domainValue) {
		if is_undefined(valuesMap[name]) {
			valuesMap[name] = {}
		}

		if is_undefined(valuesMap[name][domainName]) {
			valuesMap[name][domainName] = {}
		}

		valuesMap[name][domainName][domainValue] = true
	}


	for _, spec in specs {

		columnName := spec.name
		if !is_undefined(spec.domain) {
			for domainName, domainValue in spec.domain {
				addDomainValue(domainsValues, columnName, domainName, domainValue)
			}
		}
		if !is_undefined(spec.contextDomain) {
			for domainName, domainValue in spec.contextDomain {
				addDomainValue(contextDomainsValues, columnName, domainName, domainValue)
			}
		}


		if !is_undefined(spec.axesSpec) {
			for _, axis in spec.axesSpec {
				axisName := axis.name
				if !is_undefined(axis.domain) {
					for domainName, domainValue in axis.domain {
						addDomainValue(domainsValues, axisName, domainName, domainValue)
					}
				}
				if !is_undefined(axis.contextDomain) {
					for domainName, domainValue in axis.contextDomain {
						addDomainValue(contextDomainsValues, axisName, domainName, domainValue)
					}
				}
			}
		}
	}


	computeDiscriminative := func(valuesMap) {
		result := {}
		for name, domains in valuesMap {
			for domainName, valueSet in domains {
				if len(valueSet) > 1 {
					if is_undefined(result[name]) {
						result[name] = {}
					}
					result[name][domainName] = true
				}
			}
		}
		return result
	}

	discriminativeDomains := computeDiscriminative(domainsValues)
	discriminativeContextDomains := computeDiscriminative(contextDomainsValues)


	filterDomain := func(domain, entityName, discriminativeMap) {
		if is_undefined(domain) {
			return undefined
		}

		filteredDomain := undefined

		if !is_undefined(discriminativeMap[entityName]) {
			for domainName, domainValue in domain {
				if sets.hasElement(discriminativeMap[entityName], domainName) {
					if is_undefined(filteredDomain) {
						filteredDomain = {}
					}
					filteredDomain[domainName] = domainValue
				}
			}
		}

		return filteredDomain
	}


	distillEntity := func(entity, entityName) {
		result := maps.clone(entity)



		if !is_undefined(result.annotations) {
			linkerVal := result.annotations[A_IS_LINKER_COLUMN]
			if is_undefined(linkerVal) {
				delete(result, "annotations")
			} else {
				keptAnnotations := {}
				keptAnnotations[A_IS_LINKER_COLUMN] = linkerVal
				result.annotations = keptAnnotations
			}
		}


		filteredDomain := filterDomain(result.domain, entityName, discriminativeDomains)
		if !is_undefined(filteredDomain) {
			result.domain = filteredDomain
		} else if !is_undefined(result.domain) {
			delete(result, "domain")
		}


		filteredContextDomain := filterDomain(result.contextDomain, entityName, discriminativeContextDomains)
		if !is_undefined(filteredContextDomain) {
			result.contextDomain = filteredContextDomain
		} else if !is_undefined(result.contextDomain) {
			delete(result, "contextDomain")
		}

		return result
	}

	result := undefined
	result = ll.toStrict({






		getDiscriminativeDomains: func(name) {
			return slices.fromSet(discriminativeDomains[name])
		},







		getDiscriminativeDomainsSet: func(name) {
			return discriminativeDomains[name]
		},







		distill: func(spec) {
			result := distillEntity(spec, spec.name)


			if !is_undefined(result.axesSpec) {
				for i, axis in result.axesSpec {
					result.axesSpec[i] = distillEntity(axis, axis.name)
				}
			}

			return result
		}
	})

	return result
}















prepareAxisFiltersAndAxesSpec := func(rules, axesSpec) {
	axisFilters := []
	filteredIndices := {}

	for _, rule in rules {
		if len(rule) != 2 {
			ll.panic("Invalid filtering rule format: %v", rule)
		}

		axisIdentifier := rule[0]
		value := rule[1]

		axisIdx := axisIdentifier
		if is_string(axisIdentifier) {
			axisIdx = -1
			for idx, axis in axesSpec {
				if axis.name == axisIdentifier {
					axisIdx = idx
					break
				}
			}

			if axisIdx == -1 {
				ll.panic("Cannot find axis with name '%v' in column spec %v", axisIdentifier, axesSpec)
			}
		}

		if !is_int(axisIdx) {
			ll.panic("Axis identifier must be a number or a string that can be resolved to an index: %v", axisIdentifier)
		}

		valueStr := string(value)
		axisFilters = append(axisFilters, [axisIdx, valueStr])
		filteredIndices[axisIdx] = true
	}

	newAxesSpec := []
	for idx, axis in axesSpec {
		if !filteredIndices[idx] {
			newAxesSpec = append(newAxesSpec, maps.clone(axis))
		}
	}

	return {
		axisFilters: axisFilters,
		axesSpec: newAxesSpec
	}
}








axisSpecToMatcher := func(axisSpec) {
	validation.assertType(axisSpec, P_AXIS_SPEC_SCHEMA)
	matcher := {
		name: axisSpec.name,
		type: axisSpec.type
	}
	if !is_undefined(axisSpec.domain) {
		matcher.domain = maps.clone(axisSpec.domain) // Clone to avoid modifying original spec's domain
	}
	if !is_undefined(axisSpec.contextDomain) {
		matcher.contextDomain = maps.clone(axisSpec.contextDomain) // Clone to avoid modifying original spec's contextDomain
	}
	return matcher
}

getAxisId := func(axisSpec) {
	validation.assertType(axisSpec, P_AXIS_SPEC_SCHEMA)
	id := {
		name: axisSpec.name,
		type: axisSpec.type,
		domain: axisSpec.domain,
		contextDomain: axisSpec.contextDomain
	}
	return canonical.encode(id)
}

export ll.toStrict({
	P_UNTYPED_SPEC_SCHEMA: P_UNTYPED_SPEC_SCHEMA,
	P_AXIS_SPEC_SCHEMA: P_AXIS_SPEC_SCHEMA,
	P_COLUMN_OWN_SPEC_SCHEMA: P_COLUMN_OWN_SPEC_SCHEMA,
	P_COLUMN_SPEC_SCHEMA: P_COLUMN_SPEC_SCHEMA,
	P_COLUMN_STRICT_SPEC_SCHEMA: P_COLUMN_STRICT_SPEC_SCHEMA,
	P_OBJECT_SPEC_SCHEMA: P_OBJECT_SPEC_SCHEMA,

	KIND_P_COLUMN: KIND_P_COLUMN,

	N_LABEL: N_LABEL,

	D_BLOCK: D_BLOCK,

	A_LABEL: A_LABEL,
	A_TRACE: A_TRACE,

	normalizeColumnSpec: normalizeColumnSpec,

	matchAxis: matchAxis,
	matchAxes: matchAxes,

	makeTrace: makeTrace,

	createSpecDistiller: createSpecDistiller,

	prepareAxisFiltersAndAxesSpec: prepareAxisFiltersAndAxesSpec,
	axisSpecToMatcher: axisSpecToMatcher,

	getAxisId: getAxisId
})

