ll := import("@platforma-sdk/workflow-tengo:ll")
fmt := import("fmt")
text := import("text")
maps := import("@platforma-sdk/workflow-tengo:maps")
enum := import("enum")
smart := import("@platforma-sdk/workflow-tengo:smart")
regexp := import("@platforma-sdk/workflow-tengo:regexp")

json := import("json")





_formatKeyPath := func(current, format, key) {
	if current == "" {
		if !is_string(key) {
			return fmt.sprintf("[%d]", key)
		}

		return key
	}

	return fmt.sprintf(format, current, key)
}




success := { result: true }








fail := func(keyPath, msg, ...args) {
	return {
		result: false,
		message: keyPath + ": " + fmt.sprintf(msg, args...)
	}
}

isSuccess := func(r) {
	return r.result == true
}






isFail := func(r) {
	return r.result == false
}









check := func(condition, keyPath, msg, ...args) {
	if condition {
		return success
	}

	return fail(keyPath, msg, args...)
}












_validateTypes := func(js, tags, keyPath, ...stack) {
	k := tags.key

	if k == "any" {
		return success
	}

	if k == "alphanumeric" && ((is_string(js) && regexp.compile("^[[:alnum:]]*$").match(js)) || is_int(js)) {
		return success
	}

	if k == "string" && is_string(js) {
		regex := tags.tags["regex"]
		if !is_undefined(regex) && !regexp.compile(regex).match(js) {
			return fail(keyPath, "value %q does not conform regex %q", js, regex)
		}

		return success
	}

	if k == "char" && is_char(js) {
		return success
	}

	if k == "bytes" && is_bytes(js) {
		return success
	}

	if k == "null" && is_undefined(js) {
		return success
	}

	if k == "number" && (is_int(js) || is_float(js)) {
		return success
	}

	if (k == "bool" || k == "boolean") && is_bool(js) {
		return success
	}

	return fail(keyPath, "js %q is not one of a type: %s", js, tags.key)
}







_parseTags := func(rule) {
	tagsKV := text.split(rule, ",")
	key := tagsKV[0]
	tagsKV = tagsKV[1:]

	tags := {}
	for _, kv in tagsKV {
		kAndV := text.split(kv, "=")
		k := kAndV[0]
		if len(kAndV) == 1 {
			tags[k] = true
		} else {
			tags[k] = kAndV[1]
		}
	}

	return ll.toStrict({
		key: key,
		tags: tags
	})
}









_validateJson := func(js, schema, keyPath, ...stack) {
	if is_undefined(schema) {
		if len(stack) == 0 {
			return fail(keyPath, "key is not defined by schema, but exists in the JSON")
		}

		return fail(keyPath, "key is not defined by schema, but exists in the JSON. Validated item: %#v", stack[0])
	}



	if is_string(schema) {
		if schema == "any" {
			return success
		}
		tags := _parseTags(schema)

		return _validateTypes(js, tags, keyPath, stack...)
	}




	if is_callable(schema) {
		return schema(js, keyPath, stack)
	}




	if is_array(schema) && len(schema) > 0 && schema[0] == "or" {
		for i, elem in schema[1:] {
			r := _validateJson(js, elem, keyPath, stack...)
			if isSuccess(r) {
				return r
			}
		}

		return fail(keyPath, "js %q does not fit any of the following rules: %q", js, schema)
	}




	if is_array(schema) {
		if !is_array(js) {
			return fail(keyPath, "js %q must be array", js)
		}

		schema = schema[0]
		for i, elem in js {
			r := _validateJson(elem, schema, _formatKeyPath(keyPath, "%s[%d]", i), js, stack...)
			if isFail(r) {
				return r
			}
		}

		return success
	}





	if !ll.isMap(js) {
		return fail(keyPath, "js %#v must be map", js)
	}


	schemaTags := {}

	schemaRules := {}

	jsFull := {}

	for k, v in schema {
		p := _parseTags(k)
		schemaTags[p.key] = p
		schemaRules[p.key] = v
		jsFull[p.key] = undefined
	}


	any := schemaTags["any"]
	if !is_undefined(any) {
		if is_undefined(jsFull["any"]) { // in case actual object has field named "any"
			delete(jsFull, "any")
		}
	}

	options := schemaTags["__options__"]
	if is_undefined(options) {
		options = {}
	} else {
		delete(schemaTags, "__options__")
		delete(schemaRules, "__options__")
		if is_undefined(jsFull["__options__"]) { // in case actual object has field named "__options__"
			delete(jsFull, "__options__")
		}
	}


	for k, v in js {
		jsFull[k] = v
	}

	for _key, elem in jsFull {
		if any {
			r := _validateJson(elem, schemaRules["any"], _formatKeyPath(keyPath, "%s.%s", _key), js, stack...)
			if isFail(r) {
				return r
			}

			if !is_undefined(any.tags["type"]) {
				r := _validateJson(_key, any.tags["type"], keyPath, stack...)
				if isFail(r) {
					return r
				}
			}

			continue
		}


		key := schemaTags[_key]

		if is_undefined(key) {
			if options.tags["closed"] == true {
				return fail(keyPath, "only keys %v from the schema must be set, found %q", maps.getKeys(schema), _key)
			}

			continue
		}

		if key.tags["type"] {
			r := _validateJson(_key, key.tags["type"], keyPath, stack...)
			if isFail(r) {
				return r
			}
		}

		if is_undefined(elem) {
			if key.tags["omitempty"] || key.tags["optional"] || key.tags["?"] {
				continue
			}
			return fail(keyPath, "value %q does not contain key %q", js, _key)
		}

		r := _validateJson(elem, schemaRules[_key], _formatKeyPath(keyPath, "%s.%s", _key), js, stack...)
		if isFail(r) {
			return r
		}
	}

	return success
}








checkJson := func(js, schema) {
	return _validateJson(js, schema, "")
}








isValid := func(js, schema) {
	return _validateJson(js, schema, "").result
}




assert := func(condition, msg, ...args) {
	ll.assert(condition, msg, args...)
}

















assertType := func(value, schema, ...failMsg) {
	r := _validateJson(value, schema, "")

	msg := "type schema validation error"
	if len(failMsg) > 0 {
		msg = failMsg[0]
	}

	if isFail(r) {
		ll.panic("%s: %s", msg, r.message)
	}
}




assertJsonSchema := func(js, schema, ...failMsg) {
	assertType(js, schema, failMsg...)
}










resource := func(...args) {

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

	assertType(ops, {
		`type,?`: {
			Name: `string`,
			Version: `string`
		}
	})

	checker := func(elem, keyPath, stack) {
		c := undefined

		c = check(smart.isResource(elem), keyPath, "the element " + elem + " must be a resource")
		if isFail(c) {
			return c
		}

		if !is_undefined(ops.type) {
			c = check(elem.info().Type.Name == ops.type.Name && elem.info().Type.Version == ops.type.Version, keyPath,
				"the element " + elem + " must have the following type: ", ops.type, " found: ", elem.info().Type)
			if isFail(c) {
				return c
			}
		}

		return c
	}

	return checker
}






resourceType := func(rt) {
	return func(elem, keyPath, stack) {
		return check(smart.isResource(elem) && elem.info().Type.Name == rt.Name, keyPath, "the element " + elem + " must be a resource")
	}
}







reference := func(elem, keyPath, stack) {
	return check(smart.isReference(elem), keyPath, "the element " + elem + " must be a resource or a field")
}




refSchema := {
	blockId: `string`,
	name:  `string`
}

export ll.toStrict({
	success:                       success,
	fail:                          fail,
	isSuccess:                     isSuccess,
	isFail:                        isFail,
	checkJson:                     checkJson,
	isValid:                       isValid,
	assert:                        assert,
	assertType:                    assertType,
	assertJsonSchema:              assertJsonSchema, // deprecated
	resource:                      resource,
	reference:                     reference,
	resourceType:                  resourceType,
	ref:                           refSchema
})
