@tool
extends VBoxContainer
## Golem chat dock — AI assistant integrated in the Godot editor.

const GolemSettings = preload("golem_settings.gd")
const GolemConversation = preload("golem_conversation.gd")
const GolemAgent = preload("golem_agent.gd")
const GolemPrompts = preload("golem_prompts.gd")

const LOADING_MSGS: Array[String] = [
	"Looting",
	"Camping",
	"GLHFing",
	"Spamming F",
	"Teabagging",
	"Banning cheaters",
	"Respawning",
	"Farming XP",
	"Rage quitting",
	"Blaming lag",
	"Checking minimap",
	"Dodging AFK kick",
	"Speedrunning",
	"Nerfing the meta",
	"Buffing nodes",
	"Rerolling stats",
	"Feeding mid",
	"Stacking buffs",
	"Pulling aggro",
	"Rolling for loot",
]

# -- Design tokens --
const ACCENT := Color(0.49, 0.83, 0.77)
const ACCENT_DIM := Color(0.49, 0.83, 0.77, 0.5)
const BG_INPUT := Color(0.13, 0.13, 0.16)
const BG_PILL := Color(0.2, 0.2, 0.24)
const BG_PANEL := Color(0, 0, 0, 0.15)
const BG_CODE := Color(0.1, 0.1, 0.13)
const BG_CODE_HEADER := Color(0.14, 0.14, 0.17)
const CLR_SYSTEM := Color(0.5, 0.5, 0.55)
const CLR_ERROR := Color(1.0, 0.42, 0.42)
const CLR_CODE := Color(0.85, 0.85, 0.9)

var _api_client: Node
var _conversation: RefCounted
var _server: Node  # GolemServer, set via set_server()
var _agent: RefCounted  # GolemAgent

# Loading animation state
var _busy := false
var _busy_elapsed := 0.0
var _busy_last_switch := -1.0
var _busy_msg: String = ""

# Streaming state
var _stream_label: Label

# UI elements
var _messages_scroll: ScrollContainer
var _messages_vbox: VBoxContainer
var _input_field: TextEdit
var _send_button: Button
var _clear_button: Button
var _settings_button: Button
var _settings_panel: PanelContainer
var _status_label: Label
var _typing_indicator: Label
var _typing_tween: Tween

# Settings widgets
var _provider_option: OptionButton
var _api_key_input: LineEdit
var _base_url_input: LineEdit
var _model_input: LineEdit
var _temp_spinbox: SpinBox


func _ready() -> void:
	custom_minimum_size = Vector2(280, 300)

	_conversation = GolemConversation.new()

	_api_client = preload("golem_api_client.gd").new()
	_api_client.name = "GolemAPIClient"
	add_child(_api_client)

	_build_ui()
	_load_settings()
	set_process(false)

	if not GolemSettings.is_configured():
		_append_system("Configure your API key in Settings to get started.")
		_settings_panel.visible = true
		_settings_button.button_pressed = true
	else:
		if _conversation.load_from_file():
			_replay_messages()
		else:
			_append_system("Ready. Ask me anything about your Godot project.")


func set_server(server: Node) -> void:
	_server = server
	# Create the agent now that we have both api_client and server
	_agent = GolemAgent.new()
	_agent.setup(_api_client, _server, _conversation)
	_agent.tool_started.connect(_on_agent_tool_started)
	_agent.tool_finished.connect(_on_agent_tool_finished)
	_agent.response_text.connect(_on_agent_response_text)
	_agent.agent_error.connect(_on_agent_error)
	_agent.agent_done.connect(_on_agent_done)


func _process(_delta: float) -> void:
	if not _busy:
		return
	_busy_elapsed += _delta
	if _busy_elapsed - _busy_last_switch >= 2.0:
		_busy_last_switch = _busy_elapsed
		var new_msg := _busy_msg
		while new_msg == _busy_msg and LOADING_MSGS.size() > 1:
			new_msg = LOADING_MSGS[randi() % LOADING_MSGS.size()]
		_busy_msg = new_msg
	var dots := (int(_busy_elapsed / 0.4) % 3) + 1
	_status_label.text = _busy_msg + " " + ".".repeat(dots)


func _build_ui() -> void:
	add_theme_constant_override("separation", 6)

	# -- Toolbar --
	var toolbar := HBoxContainer.new()
	toolbar.add_theme_constant_override("separation", 8)
	toolbar.alignment = BoxContainer.ALIGNMENT_CENTER
	add_child(toolbar)

	# Thinking / loading label (top-left)
	_status_label = Label.new()
	_status_label.text = ""
	_status_label.add_theme_color_override("font_color", ACCENT)
	toolbar.add_child(_status_label)

	var spacer := Control.new()
	spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	toolbar.add_child(spacer)

	_clear_button = Button.new()
	_clear_button.text = "Clear"
	_clear_button.flat = true
	_clear_button.pressed.connect(_on_clear_pressed)
	toolbar.add_child(_clear_button)

	_settings_button = Button.new()
	_settings_button.text = "Settings"
	_settings_button.flat = true
	_settings_button.toggle_mode = true
	_settings_button.toggled.connect(_on_settings_toggled)
	toolbar.add_child(_settings_button)

	# -- Settings panel (hidden by default) --
	_settings_panel = PanelContainer.new()
	_settings_panel.visible = false
	add_child(_settings_panel)

	var settings_vbox := VBoxContainer.new()
	settings_vbox.add_theme_constant_override("separation", 6)
	_settings_panel.add_child(settings_vbox)

	var provider_row := _make_row("Provider")
	settings_vbox.add_child(provider_row)
	_provider_option = OptionButton.new()
	_provider_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	var idx := 0
	for key in GolemSettings.PROVIDERS:
		_provider_option.add_item(GolemSettings.PROVIDERS[key]["name"], idx)
		_provider_option.set_item_metadata(idx, key)
		idx += 1
	_provider_option.item_selected.connect(_on_provider_selected)
	provider_row.add_child(_provider_option)

	var key_row := _make_row("API Key")
	settings_vbox.add_child(key_row)
	_api_key_input = LineEdit.new()
	_api_key_input.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	_api_key_input.secret = true
	_api_key_input.placeholder_text = "sk-..."
	key_row.add_child(_api_key_input)

	var url_row := _make_row("Base URL")
	settings_vbox.add_child(url_row)
	_base_url_input = LineEdit.new()
	_base_url_input.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	url_row.add_child(_base_url_input)

	var model_row := _make_row("Model")
	settings_vbox.add_child(model_row)
	_model_input = LineEdit.new()
	_model_input.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	model_row.add_child(_model_input)

	var temp_row := _make_row("Temp")
	settings_vbox.add_child(temp_row)
	_temp_spinbox = SpinBox.new()
	_temp_spinbox.min_value = 0.0
	_temp_spinbox.max_value = 2.0
	_temp_spinbox.step = 0.1
	_temp_spinbox.value = 0.7
	_temp_spinbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	temp_row.add_child(_temp_spinbox)

	var save_btn := Button.new()
	save_btn.text = "Save Settings"
	save_btn.pressed.connect(_on_save_settings)
	settings_vbox.add_child(save_btn)

	# -- Messages area --
	var messages_panel := PanelContainer.new()
	messages_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
	var panel_style := StyleBoxFlat.new()
	panel_style.bg_color = BG_PANEL
	panel_style.set_corner_radius_all(6)
	panel_style.content_margin_left = 10
	panel_style.content_margin_right = 10
	panel_style.content_margin_top = 10
	panel_style.content_margin_bottom = 10
	messages_panel.add_theme_stylebox_override("panel", panel_style)
	add_child(messages_panel)

	_messages_scroll = ScrollContainer.new()
	_messages_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
	_messages_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	_messages_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
	messages_panel.add_child(_messages_scroll)

	_messages_vbox = VBoxContainer.new()
	_messages_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	_messages_vbox.add_theme_constant_override("separation", 10)
	_messages_scroll.add_child(_messages_vbox)

	# -- Input area --
	var input_row := HBoxContainer.new()
	input_row.add_theme_constant_override("separation", 6)
	add_child(input_row)

	_input_field = TextEdit.new()
	_input_field.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	_input_field.placeholder_text = "Ask Golem..."
	_input_field.custom_minimum_size.y = 48
	_input_field.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
	_input_field.scroll_fit_content_height = true
	_input_field.gui_input.connect(_on_input_gui_input)
	_input_field.text_changed.connect(_on_input_text_changed)

	var input_normal := StyleBoxFlat.new()
	input_normal.bg_color = BG_INPUT
	input_normal.set_border_width_all(0)
	input_normal.set_corner_radius_all(8)
	input_normal.content_margin_left = 12
	input_normal.content_margin_right = 12
	input_normal.content_margin_top = 8
	input_normal.content_margin_bottom = 8
	_input_field.add_theme_stylebox_override("normal", input_normal)

	var input_focus: StyleBoxFlat = input_normal.duplicate()
	input_focus.border_color = ACCENT_DIM
	input_focus.set_border_width_all(1)
	_input_field.add_theme_stylebox_override("focus", input_focus)

	var input_readonly: StyleBoxFlat = input_normal.duplicate()
	input_readonly.bg_color = Color(0.1, 0.1, 0.12)
	_input_field.add_theme_stylebox_override("read_only", input_readonly)

	input_row.add_child(_input_field)

	# Circular send button
	_send_button = Button.new()
	_send_button.text = "↑"
	_send_button.custom_minimum_size = Vector2(36, 0)
	_send_button.pressed.connect(_on_send_pressed)

	var btn_normal := StyleBoxFlat.new()
	btn_normal.bg_color = ACCENT
	btn_normal.set_corner_radius_all(14)
	btn_normal.content_margin_left = 10
	btn_normal.content_margin_right = 10
	btn_normal.content_margin_top = 8
	btn_normal.content_margin_bottom = 8
	_send_button.add_theme_stylebox_override("normal", btn_normal)
	_send_button.add_theme_color_override("font_color", Color(0.05, 0.05, 0.07))

	var btn_hover: StyleBoxFlat = btn_normal.duplicate()
	btn_hover.bg_color = ACCENT.lightened(0.15)
	_send_button.add_theme_stylebox_override("hover", btn_hover)

	var btn_pressed: StyleBoxFlat = btn_normal.duplicate()
	btn_pressed.bg_color = ACCENT.darkened(0.15)
	_send_button.add_theme_stylebox_override("pressed", btn_pressed)

	var btn_disabled: StyleBoxFlat = btn_normal.duplicate()
	btn_disabled.bg_color = Color(0.3, 0.3, 0.35)
	_send_button.add_theme_stylebox_override("disabled", btn_disabled)
	_send_button.add_theme_color_override("font_disabled_color", Color(0.5, 0.5, 0.55))

	input_row.add_child(_send_button)


func _make_row(label_text: String) -> HBoxContainer:
	var row := HBoxContainer.new()
	row.add_theme_constant_override("separation", 8)
	var label := Label.new()
	label.text = label_text
	label.custom_minimum_size = Vector2(65, 0)
	row.add_child(label)
	return row


# ------------------------------------------------------------------
# Settings
# ------------------------------------------------------------------

func _load_settings() -> void:
	var provider := GolemSettings.get_provider()
	for i in range(_provider_option.item_count):
		if _provider_option.get_item_metadata(i) == provider:
			_provider_option.selected = i
			break
	_api_key_input.text = GolemSettings.get_api_key()
	_base_url_input.text = GolemSettings.get_base_url()
	_model_input.text = GolemSettings.get_model()
	_temp_spinbox.value = GolemSettings.get_temperature()


func _on_provider_selected(index: int) -> void:
	var key: String = _provider_option.get_item_metadata(index)
	var preset: Dictionary = GolemSettings.PROVIDERS.get(key, {})
	_base_url_input.text = preset.get("base_url", "")
	_model_input.text = preset.get("default_model", "")
	if key == "ollama":
		_api_key_input.placeholder_text = "(not required)"
	else:
		_api_key_input.placeholder_text = "sk-..."


func _on_save_settings() -> void:
	var provider_key: String = _provider_option.get_item_metadata(_provider_option.selected)
	GolemSettings.set_provider(provider_key)
	GolemSettings.set_api_key(_api_key_input.text)
	GolemSettings.set_base_url(_base_url_input.text)
	GolemSettings.set_model(_model_input.text)
	GolemSettings.set_temperature(_temp_spinbox.value)
	_append_system("Settings saved.")
	_settings_panel.visible = false
	_settings_button.button_pressed = false


func _on_settings_toggled(toggled: bool) -> void:
	_settings_panel.visible = toggled


# ------------------------------------------------------------------
# Chat
# ------------------------------------------------------------------

func _on_input_gui_input(event: InputEvent) -> void:
	if event is InputEventKey and event.pressed and not event.is_echo():
		if event.keycode == KEY_ENTER and not event.shift_pressed:
			get_viewport().set_input_as_handled()
			_send_message()


func _on_input_text_changed() -> void:
	var content_h: int = _input_field.get_line_count() * _input_field.get_line_height() + 16
	_input_field.custom_minimum_size.y = clamp(content_h, 48, 120)


func _on_send_pressed() -> void:
	_send_message()


func _send_message() -> void:
	var text := _input_field.text.strip_edges()
	if text.is_empty():
		return

	if not GolemSettings.is_configured():
		_append_error("Please configure your API key in Settings first.")
		return

	_input_field.text = ""
	_input_field.custom_minimum_size.y = 48
	_append_user(text)
	_conversation.add_user_message(text)
	_conversation.set_system_prompt(_build_system_prompt())

	_set_busy(true)

	if _agent != null:
		# Agent mode: tool calling + non-streaming LLM rounds
		_agent.run()
	else:
		# Fallback: streaming chat only (no server / no tools)
		_stream_label = Label.new()
		_stream_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
		_stream_label.add_theme_color_override("font_color", CLR_CODE)
		_messages_vbox.add_child(_stream_label)

		_api_client.stream_chunk.connect(_on_stream_chunk)
		_api_client.stream_done.connect(_on_stream_done)
		_api_client.stream_error.connect(_on_stream_error)
		_api_client.start_stream(_conversation.get_messages())


func _on_stream_chunk(text: String) -> void:
	_remove_typing_indicator()
	if is_instance_valid(_stream_label):
		_stream_label.text += text
		_scroll_to_bottom()


func _on_stream_done(full_text: String) -> void:
	_disconnect_stream_signals()
	_set_busy(false)

	if is_instance_valid(_stream_label):
		_messages_vbox.remove_child(_stream_label)
		_stream_label.queue_free()
	_stream_label = null

	_conversation.add_assistant_message(full_text)
	_append_assistant(full_text)
	_conversation.save_to_file()
	_input_field.grab_focus()


func _on_stream_error(message: String) -> void:
	_disconnect_stream_signals()
	_set_busy(false)

	if is_instance_valid(_stream_label):
		_messages_vbox.remove_child(_stream_label)
		_stream_label.queue_free()
	_stream_label = null

	_append_error(message)
	_input_field.grab_focus()


func _disconnect_stream_signals() -> void:
	if _api_client.stream_chunk.is_connected(_on_stream_chunk):
		_api_client.stream_chunk.disconnect(_on_stream_chunk)
	if _api_client.stream_done.is_connected(_on_stream_done):
		_api_client.stream_done.disconnect(_on_stream_done)
	if _api_client.stream_error.is_connected(_on_stream_error):
		_api_client.stream_error.disconnect(_on_stream_error)


# ------------------------------------------------------------------
# Agent callbacks
# ------------------------------------------------------------------

func _on_agent_tool_started(tool_name: String, _args: Dictionary) -> void:
	_remove_typing_indicator()
	_append_tool_pill(tool_name, true)


func _on_agent_tool_finished(tool_name: String, _result: String, _is_image: bool) -> void:
	# Update the last tool pill to show completion
	_update_last_tool_pill(tool_name, false)


func _on_agent_response_text(text: String) -> void:
	_remove_typing_indicator()
	if not text.is_empty():
		_append_assistant(text)


func _on_agent_error(message: String) -> void:
	_remove_typing_indicator()
	_append_error(message)


func _on_agent_done() -> void:
	_set_busy(false)
	_conversation.save_to_file()
	_input_field.grab_focus()


func _append_tool_pill(tool_name: String, running: bool) -> void:
	var pill := PanelContainer.new()
	pill.name = "ToolPill"
	var style := StyleBoxFlat.new()
	style.bg_color = Color(0.18, 0.22, 0.28) if running else Color(0.15, 0.19, 0.22)
	style.set_corner_radius_all(10)
	style.content_margin_left = 10
	style.content_margin_right = 10
	style.content_margin_top = 4
	style.content_margin_bottom = 4
	pill.add_theme_stylebox_override("panel", style)

	var label := Label.new()
	label.name = "ToolLabel"
	if running:
		label.text = "Calling %s..." % tool_name
	else:
		label.text = "%s done" % tool_name
	label.add_theme_color_override("font_color", ACCENT_DIM)
	pill.add_child(label)

	_messages_vbox.add_child(pill)
	_scroll_to_bottom()


func _update_last_tool_pill(tool_name: String, _running: bool) -> void:
	# Find the last ToolPill and update its label
	for i in range(_messages_vbox.get_child_count() - 1, -1, -1):
		var child: Node = _messages_vbox.get_child(i)
		if child.name == "ToolPill":
			var label: Variant = child.get_node_or_null("ToolLabel")
			if label is Label:
				label.text = "%s done" % tool_name
				var pill_style: Variant = child.get_theme_stylebox("panel")
				if pill_style is StyleBoxFlat:
					pill_style.bg_color = Color(0.15, 0.19, 0.22)
			break


func _on_clear_pressed() -> void:
	# Stop streaming if active
	if _api_client.is_busy():
		_api_client.stop_stream()
		_disconnect_stream_signals()
		_set_busy(false)
		_stream_label = null

	# Note: agent running state resets on next run() call — no explicit stop needed.

	for child in _messages_vbox.get_children():
		_messages_vbox.remove_child(child)
		child.queue_free()
	_conversation.clear()
	_conversation.delete_save()
	_append_system("Conversation cleared.")
	_input_field.grab_focus()


func _replay_messages() -> void:
	var msgs: Array = _conversation.get_messages()
	for i in range(1, msgs.size()):
		var msg: Variant = msgs[i]
		if not msg is Dictionary:
			continue
		var role: String = msg.get("role", "")
		var content: Variant = msg.get("content", "")
		if role == "user":
			_append_user(str(content))
		elif role == "assistant":
			# Assistant messages with tool_calls have null content
			if msg.has("tool_calls"):
				var tool_calls: Variant = msg["tool_calls"]
				if tool_calls is Array:
					for tc in tool_calls:
						if tc is Dictionary:
							var fn: Variant = tc.get("function", {})
							if fn is Dictionary:
								_append_tool_pill(fn.get("name", "tool"), false)
			elif content != null and not str(content).is_empty():
				_append_assistant(str(content))
		elif role == "tool":
			pass  # Tool results are shown implicitly via tool pills


func _build_system_prompt() -> String:
	var context := _gather_context()
	var user_msg := _get_last_user_message()
	return GolemPrompts.build(user_msg, context)


func _gather_context() -> Dictionary:
	var ctx: Dictionary = {}

	var project_name: String = ProjectSettings.get_setting("application/config/name", "")
	if not project_name.is_empty():
		ctx["project_name"] = project_name

	var edited_root: Node = EditorInterface.get_edited_scene_root()
	if edited_root:
		ctx["scene_path"] = edited_root.scene_file_path

	ctx["has_event_bus"] = FileAccess.file_exists("res://core/event_bus.gd")
	ctx["has_game_plan"] = FileAccess.file_exists("res://GAME_PLAN.md")
	ctx["has_theme"] = (
		FileAccess.file_exists("res://theme.tres")
		or FileAccess.file_exists("res://resources/theme.tres")
		or FileAccess.file_exists("res://ui/theme.tres")
	)

	return ctx


func _get_last_user_message() -> String:
	var msgs: Array = _conversation.get_messages()
	for i in range(msgs.size() - 1, -1, -1):
		var msg: Variant = msgs[i]
		if msg is Dictionary and msg.get("role") == "user":
			return str(msg.get("content", ""))
	return ""


# ------------------------------------------------------------------
# Display helpers
# ------------------------------------------------------------------

func _append_user(text: String) -> void:
	var pill := PanelContainer.new()
	var style := StyleBoxFlat.new()
	style.bg_color = BG_PILL
	style.set_corner_radius_all(14)
	style.content_margin_left = 14
	style.content_margin_right = 14
	style.content_margin_top = 6
	style.content_margin_bottom = 6
	pill.add_theme_stylebox_override("panel", style)

	var label := Label.new()
	label.text = text
	label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	pill.add_child(label)

	_messages_vbox.add_child(pill)
	_scroll_to_bottom()


func _append_assistant(text: String) -> void:
	var container := VBoxContainer.new()
	container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	container.add_theme_constant_override("separation", 4)

	var segments: Array = _split_segments(text)
	for seg in segments:
		if not seg is Dictionary:
			continue
		if seg.get("type") == "code":
			container.add_child(_build_code_block(
				seg.get("lang", ""), seg.get("content", "")
			))
		else:
			var seg_text: String = seg.get("content", "")
			if seg_text.strip_edges().is_empty():
				continue
			var rtl := RichTextLabel.new()
			rtl.bbcode_enabled = true
			rtl.fit_content = true
			rtl.scroll_active = false
			rtl.selection_enabled = true
			rtl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
			rtl.append_text(_format_text_bbcode(seg_text))
			container.add_child(rtl)

	_messages_vbox.add_child(container)
	_scroll_to_bottom()


func _build_code_block(lang: String, code: String) -> VBoxContainer:
	var block := VBoxContainer.new()
	block.add_theme_constant_override("separation", 0)

	# Header bar
	var header := PanelContainer.new()
	var hstyle := StyleBoxFlat.new()
	hstyle.bg_color = BG_CODE_HEADER
	hstyle.corner_radius_top_left = 8
	hstyle.corner_radius_top_right = 8
	hstyle.content_margin_left = 10
	hstyle.content_margin_right = 6
	hstyle.content_margin_top = 4
	hstyle.content_margin_bottom = 4
	header.add_theme_stylebox_override("panel", hstyle)

	var hbox := HBoxContainer.new()
	header.add_child(hbox)

	var lang_label := Label.new()
	var display_lang := lang
	if display_lang.is_empty():
		display_lang = "code"
	lang_label.text = display_lang
	lang_label.add_theme_color_override("font_color", CLR_SYSTEM)
	lang_label.add_theme_font_size_override("font_size", 13)
	hbox.add_child(lang_label)

	var hspacer := Control.new()
	hspacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	hbox.add_child(hspacer)

	var copy_btn := Button.new()
	copy_btn.text = "Copy"
	copy_btn.flat = true
	copy_btn.add_theme_color_override("font_color", CLR_SYSTEM)
	copy_btn.add_theme_font_size_override("font_size", 13)
	copy_btn.pressed.connect(_on_copy_code.bind(copy_btn, code))
	hbox.add_child(copy_btn)

	block.add_child(header)

	# Code body
	var body := PanelContainer.new()
	var bstyle := StyleBoxFlat.new()
	bstyle.bg_color = BG_CODE
	bstyle.corner_radius_bottom_left = 8
	bstyle.corner_radius_bottom_right = 8
	bstyle.content_margin_left = 10
	bstyle.content_margin_right = 10
	bstyle.content_margin_top = 8
	bstyle.content_margin_bottom = 8
	body.add_theme_stylebox_override("panel", bstyle)

	var code_label := Label.new()
	code_label.text = code
	code_label.autowrap_mode = TextServer.AUTOWRAP_ARBITRARY
	code_label.add_theme_color_override("font_color", CLR_CODE)
	body.add_child(code_label)

	block.add_child(body)
	return block


func _on_copy_code(btn: Button, code: String) -> void:
	DisplayServer.clipboard_set(code)
	btn.text = "Copied!"
	var tree := get_tree()
	if tree:
		tree.create_timer(1.5).timeout.connect(func():
			if is_instance_valid(btn):
				btn.text = "Copy"
		)


func _split_segments(text: String) -> Array:
	var segments: Array = []
	var lines: PackedStringArray = text.split("\n")
	var current_text := ""
	var in_code := false
	var code_lines: PackedStringArray = []
	var code_lang := ""

	for line in lines:
		var stripped := line.strip_edges()
		if stripped.begins_with("```"):
			if in_code:
				segments.append({
					"type": "code", "lang": code_lang,
					"content": "\n".join(code_lines),
				})
				code_lines.clear()
				in_code = false
			else:
				if not current_text.strip_edges().is_empty():
					segments.append({"type": "text", "content": current_text})
				current_text = ""
				code_lang = stripped.substr(3).strip_edges()
				in_code = true
			continue

		if in_code:
			code_lines.append(line)
		else:
			current_text += line + "\n"

	# Flush remaining content
	if in_code and code_lines.size() > 0:
		segments.append({
			"type": "code", "lang": code_lang,
			"content": "\n".join(code_lines),
		})
	elif not current_text.strip_edges().is_empty():
		segments.append({"type": "text", "content": current_text})

	return segments


func _append_error(text: String) -> void:
	var label := Label.new()
	label.text = "Error: " + text
	label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	label.add_theme_color_override("font_color", CLR_ERROR)
	_messages_vbox.add_child(label)
	_scroll_to_bottom()


func _append_system(text: String) -> void:
	var label := Label.new()
	label.text = text
	label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	label.add_theme_color_override("font_color", CLR_SYSTEM)
	_messages_vbox.add_child(label)
	_scroll_to_bottom()


func _set_busy(busy: bool) -> void:
	_busy = busy
	_send_button.disabled = busy
	_input_field.editable = not busy
	if busy:
		_busy_elapsed = 0.0
		_busy_last_switch = -1.0
		_busy_msg = LOADING_MSGS[randi() % LOADING_MSGS.size()]
		_status_label.text = _busy_msg + " ."
		set_process(true)
		# Pulsing ● in the messages area
		_typing_indicator = Label.new()
		_typing_indicator.text = "●"
		_typing_indicator.add_theme_color_override("font_color", ACCENT)
		_typing_indicator.add_theme_font_size_override("font_size", 18)
		_messages_vbox.add_child(_typing_indicator)
		_scroll_to_bottom()
		_typing_tween = create_tween().set_loops()
		_typing_tween.tween_property(_typing_indicator, "modulate:a", 0.3, 0.5).set_trans(Tween.TRANS_SINE)
		_typing_tween.tween_property(_typing_indicator, "modulate:a", 1.0, 0.5).set_trans(Tween.TRANS_SINE)
	else:
		set_process(false)
		_status_label.text = ""
		_remove_typing_indicator()
		_input_field.grab_focus()


func _remove_typing_indicator() -> void:
	if is_instance_valid(_typing_indicator):
		if _typing_tween and _typing_tween.is_valid():
			_typing_tween.kill()
		_typing_tween = null
		_messages_vbox.remove_child(_typing_indicator)
		_typing_indicator.queue_free()
		_typing_indicator = null


func _scroll_to_bottom() -> void:
	# Wait one frame for layout to recalculate content size, then scroll to actual max
	get_tree().process_frame.connect(func():
		_messages_scroll.scroll_vertical = int(_messages_scroll.get_v_scroll_bar().max_value)
	, CONNECT_ONE_SHOT)


# ------------------------------------------------------------------
# Markdown -> BBCode
# ------------------------------------------------------------------

func _format_text_bbcode(text: String) -> String:
	var result := ""
	var lines := text.split("\n")

	for line in lines:
		if line.strip_edges().is_empty():
			result += "\n"
			continue
		result += _md_inline(line) + "\n"

	return result


func _md_inline(line: String) -> String:
	if line.begins_with("### "):
		return "[b]" + _escape_bbcode(line.substr(4)) + "[/b]"
	if line.begins_with("## "):
		return "[b]" + _escape_bbcode(line.substr(3)) + "[/b]"
	if line.begins_with("# "):
		return "[b]" + _escape_bbcode(line.substr(2)) + "[/b]"

	var stripped := line.strip_edges()

	# Bullet list
	if stripped.begins_with("- ") or stripped.begins_with("* "):
		var indent := line.length() - line.strip_edges(true, false).length()
		var prefix := "  ".repeat(indent / 2) + "•  "
		return prefix + _format_inline(_escape_bbcode(stripped.substr(2)))

	# Numbered list (1. , 2. , etc.)
	var dot_pos: int = stripped.find(". ")
	if dot_pos > 0 and dot_pos <= 3:
		var num_part := stripped.substr(0, dot_pos)
		if num_part.is_valid_int():
			var indent := line.length() - line.strip_edges(true, false).length()
			var prefix := "  ".repeat(indent / 2)
			return prefix + num_part + ". " + _format_inline(_escape_bbcode(stripped.substr(dot_pos + 2)))

	return _format_inline(_escape_bbcode(line))


func _format_inline(text: String) -> String:
	var result := text
	result = _replace_pattern(result, "**", "[b]", "[/b]")
	result = _replace_pattern(result, "*", "[i]", "[/i]")
	result = _replace_pattern(result, "`", "[code]", "[/code]")
	return result


func _replace_pattern(text: String, marker: String, open_tag: String, close_tag: String) -> String:
	var result := ""
	var search_from := 0
	while true:
		var start := text.find(marker, search_from)
		if start == -1:
			result += text.substr(search_from)
			break
		var end := text.find(marker, start + marker.length())
		if end == -1:
			result += text.substr(search_from)
			break
		result += text.substr(search_from, start - search_from)
		result += open_tag + text.substr(start + marker.length(), end - start - marker.length()) + close_tag
		search_from = end + marker.length()
	return result


func _escape_bbcode(text: String) -> String:
	return text.replace("[", "[lb]")
