class_name TagExtractor
extends RefCounted
## Pure §12 tag parsing. Extracts [FACT:], [ADJUST_DISPOSITION:], [MOVE:] tags
## and returns the prose with all tags stripped. It never applies anything and
## never validates a move (that needs NPC context from /npc/speak — a later
## plan). Its outputs are exactly the inputs CanonLog's mutators consume.

static var _fact_re: RegEx = RegEx.create_from_string("\\[FACT:\\s*(.*?)\\]")
static var _dispo_re: RegEx = RegEx.create_from_string("\\[ADJUST_DISPOSITION:\\s*([+-]?\\d+)\\]")
static var _move_re: RegEx = RegEx.create_from_string("\\[MOVE:\\s*(\\w+)\\(([^)]*)\\)\\]")
static var _any_re: RegEx = RegEx.create_from_string("\\[[A-Z_]+:[^\\]]*\\]")


static func extract(prose: String) -> Dictionary:
	var facts: Array = []
	for m in _fact_re.search_all(prose):
		facts.append(m.get_string(1).strip_edges())

	var dispositions: Array = []
	for m in _dispo_re.search_all(prose):
		dispositions.append(int(m.get_string(1)))

	var moves: Array = []
	for m in _move_re.search_all(prose):
		var raw_args := m.get_string(2).strip_edges()
		var args: Array = []
		if raw_args != "":
			for a in raw_args.split(","):
				args.append(a.strip_edges())
		moves.append({"name": m.get_string(1), "args": args})

	var clean := _any_re.sub(prose, "", true)
	# Collapse whitespace left where inline tags were removed.
	while clean.contains("  "):
		clean = clean.replace("  ", " ")
	clean = clean.strip_edges()

	return {"clean_text": clean, "facts": facts, "dispositions": dispositions, "moves": moves}
