class_name AlertsLog extends CanvasLayer
## Phase 17 — Scrollable alerts / event log bottom-sheet.
##
## Opened via TopBar "Log" button (injected by main.gd).
## Subscribes to:
##   EventBus.alert_added(severity, text, focus_tile)
##   EventBus.storyteller_event_fired(event) — translated to an alert entry
##   EventBus.day_ended(summary) — produces a one-line day-summary entry
##
## Internal ring buffer capped at RING_CAP (50) entries; oldest dropped.
## Session-scoped — no save/load round-trip required for MVP.
##
## Severity icons (inline text prefix):
##   info   → "[I]" blue
##   warn   → "[!]" yellow
##   danger → "[!!]" red
##
## Timestamp format: "Day D, HH:MM"

const RING_CAP: int = 50
const PANEL_HEIGHT: float = 500.0

const SEVERITY_COLORS: Dictionary = {
	&"info":   Color(0.30, 0.55, 0.95, 1.0),
	&"warn":   Color(0.95, 0.80, 0.10, 1.0),
	&"danger": Color(0.90, 0.15, 0.10, 1.0),
}
const SEVERITY_PREFIX: Dictionary = {
	&"info":   "[I]",
	&"warn":   "[!]",
	&"danger": "[!!]",
}

## Maps EventDef.Category → default alert severity for storyteller events.
const STORYTELLER_SEVERITY: Dictionary = {
	0: &"info",    # NUDGE
	1: &"info",    # SEASONAL
	2: &"info",    # WANDERER
	3: &"danger",  # THREAT
	4: &"warn",    # DISEASE
	5: &"info",    # RESOURCE
	6: &"info",    # LORE
	7: &"info",    # MILESTONE
}

## Unread badge count — increments on add, resets on open.
var _unread_count: int = 0

## Ring buffer of alert entry dicts:
##   { "severity": StringName, "timestamp": String, "text": String, "focus_tile": Vector2i }
var _entries: Array = []

var _root: Control = null
var _scroll: ScrollContainer = null
var _vbox: VBoxContainer = null
var _badge_btn: Button = null
## The button node injected from TopBar (the "Log" button).
## AlertsLog updates its text with the unread count badge.
var log_button: Button = null


func _ready() -> void:
	layer = 19
	_build_ui()
	# Hide the whole CanvasLayer when closed so the Backdrop ColorRect (which
	# has MOUSE_FILTER_STOP for click-outside-to-close) does not eat mouse
	# events in `_unhandled_input` for the world below.
	visible = false
	_root.visible = false

	EventBus.alert_added.connect(_on_alert_added)
	if EventBus.has_signal("storyteller_event_fired"):
		EventBus.storyteller_event_fired.connect(_on_storyteller_event)
	if EventBus.has_signal("day_ended"):
		EventBus.day_ended.connect(_on_day_ended)
	if EventBus.has_signal("room_too_large"):
		EventBus.room_too_large.connect(_on_room_too_large)
		# Catch-up: room_too_large emits during World init (cabin demo room),
		# which runs before this CanvasLayer mounts. Replay existing too-large
		# rooms now so the boot-state cabin warning shows up in the log.
		_catch_up_room_too_large()
	if EventBus.has_signal("no_stockpile_accepts"):
		EventBus.no_stockpile_accepts.connect(_on_no_stockpile_accepts)
	if EventBus.has_signal("bill_blocked"):
		EventBus.bill_blocked.connect(_on_bill_blocked)

	Audit.log("alerts_log", "AlertsLog ready")


func _exit_tree() -> void:
	if EventBus.alert_added.is_connected(_on_alert_added):
		EventBus.alert_added.disconnect(_on_alert_added)
	if EventBus.has_signal("storyteller_event_fired") and EventBus.storyteller_event_fired.is_connected(_on_storyteller_event):
		EventBus.storyteller_event_fired.disconnect(_on_storyteller_event)
	if EventBus.has_signal("day_ended") and EventBus.day_ended.is_connected(_on_day_ended):
		EventBus.day_ended.disconnect(_on_day_ended)
	if EventBus.has_signal("room_too_large") and EventBus.room_too_large.is_connected(_on_room_too_large):
		EventBus.room_too_large.disconnect(_on_room_too_large)
	if EventBus.has_signal("no_stockpile_accepts") and EventBus.no_stockpile_accepts.is_connected(_on_no_stockpile_accepts):
		EventBus.no_stockpile_accepts.disconnect(_on_no_stockpile_accepts)
	if EventBus.has_signal("bill_blocked") and EventBus.bill_blocked.is_connected(_on_bill_blocked):
		EventBus.bill_blocked.disconnect(_on_bill_blocked)


# ── public API ────────────────────────────────────────────────────────────────

func open() -> void:
	_rebuild_list()
	visible = true
	_root.visible = true
	_unread_count = 0
	_update_badge()
	Audit.log("alerts_log", "opened (entries=%d)" % _entries.size())


func close() -> void:
	visible = false
	_root.visible = false
	Audit.log("alerts_log", "closed")


# ── UI construction ───────────────────────────────────────────────────────────

func _build_ui() -> void:
	var backdrop := ColorRect.new()
	backdrop.name = "Backdrop"
	backdrop.set_anchors_preset(Control.PRESET_FULL_RECT)
	backdrop.color = Color(0.0, 0.0, 0.0, 0.45)
	backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
	backdrop.gui_input.connect(_on_backdrop_input)
	add_child(backdrop)

	_root = Control.new()
	_root.name = "LogPanel"
	_root.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
	_root.custom_minimum_size = Vector2(0.0, PANEL_HEIGHT)
	_root.offset_top = -PANEL_HEIGHT
	_root.offset_bottom = 0.0
	_root.mouse_filter = Control.MOUSE_FILTER_STOP
	backdrop.add_child(_root)

	var bg := PanelContainer.new()
	bg.name = "BG"
	bg.set_anchors_preset(Control.PRESET_FULL_RECT)
	_root.add_child(bg)

	var outer_vbox := VBoxContainer.new()
	outer_vbox.add_theme_constant_override("separation", 4)
	bg.add_child(outer_vbox)

	# Header.
	var header := HBoxContainer.new()
	header.add_theme_constant_override("separation", 8)
	outer_vbox.add_child(header)

	var title := Label.new()
	title.text = "Alerts"
	title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	header.add_child(title)

	var close_btn := Button.new()
	close_btn.text = "Close"
	close_btn.focus_mode = Control.FOCUS_NONE
	close_btn.custom_minimum_size = Vector2(72.0, 36.0)
	close_btn.pressed.connect(close)
	header.add_child(close_btn)

	# Scrollable entry list.
	_scroll = ScrollContainer.new()
	_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
	outer_vbox.add_child(_scroll)

	_vbox = VBoxContainer.new()
	_vbox.add_theme_constant_override("separation", 4)
	_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	_scroll.add_child(_vbox)


# ── entry management ──────────────────────────────────────────────────────────

func _push_entry(severity: StringName, text: String, focus_tile: Vector2i) -> void:
	var entry: Dictionary = {
		"severity":   severity,
		"timestamp":  _format_timestamp(),
		"text":       text,
		"focus_tile": focus_tile,
	}
	_entries.push_front(entry)  # newest first
	if _entries.size() > RING_CAP:
		_entries.resize(RING_CAP)
	_unread_count += 1
	_update_badge()


func _rebuild_list() -> void:
	for child in _vbox.get_children():
		child.queue_free()
	for entry in _entries:
		_add_entry_row(entry)


func _add_entry_row(entry: Dictionary) -> void:
	var sev: StringName = entry.get("severity", &"info")
	var row := HBoxContainer.new()
	row.add_theme_constant_override("separation", 6)
	_vbox.add_child(row)

	# Severity color icon label.
	var icon_lbl := Label.new()
	icon_lbl.text = SEVERITY_PREFIX.get(sev, "[i]")
	icon_lbl.modulate = SEVERITY_COLORS.get(sev, Color.WHITE)
	icon_lbl.custom_minimum_size = Vector2(32.0, 0.0)
	row.add_child(icon_lbl)

	# Timestamp.
	var ts_lbl := Label.new()
	ts_lbl.text = entry.get("timestamp", "")
	ts_lbl.custom_minimum_size = Vector2(90.0, 0.0)
	ts_lbl.add_theme_font_size_override("font_size", 11)
	row.add_child(ts_lbl)

	# Text — expands to fill.
	var text_lbl := Label.new()
	text_lbl.text = entry.get("text", "")
	text_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	text_lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	row.add_child(text_lbl)

	# "Go there" button if focus tile is valid.
	var ft: Vector2i = entry.get("focus_tile", Vector2i(-1, -1))
	if ft != Vector2i(-1, -1):
		var go_btn := Button.new()
		go_btn.text = "Go"
		go_btn.focus_mode = Control.FOCUS_NONE
		go_btn.custom_minimum_size = Vector2(48.0, 32.0)
		var tile := ft
		go_btn.pressed.connect(func() -> void: _pan_to(tile))
		row.add_child(go_btn)


# ── signal handlers ───────────────────────────────────────────────────────────

func _on_alert_added(severity: StringName, text: String, focus_tile: Vector2i) -> void:
	_push_entry(severity, text, focus_tile)
	Audit.log("alerts_log", "[%s] %s" % [severity, text])


func _on_storyteller_event(event) -> void:
	# Translate EventDef → alert entry. severity derived from category enum value.
	var cat_int: int = int(event.get("category") if event.get("category") != null else 0)
	var sev: StringName = STORYTELLER_SEVERITY.get(cat_int, &"info")
	var title_text: String = event.get("title") if event.get("title") != null else "Event"
	var body_text: String  = event.get("body")  if event.get("body")  != null else ""
	var ft: Vector2i = Vector2i(-1, -1)
	if event.get("focus_tile") != null:
		ft = event.focus_tile
	_push_entry(sev, "%s — %s" % [title_text, body_text], ft)


func _on_day_ended(summary: Dictionary) -> void:
	var day: int     = int(summary.get("day", 0))
	var season: StringName = StringName(summary.get("season", &""))
	var pawns: int   = int(summary.get("pawns_alive", 0))
	var tension: float = float(summary.get("tension", 0.0))
	var wolves: int  = int(summary.get("wolves_alive", 0))
	var text: String = "Day %d ended — %s — pawns: %d, wolves: %d, tension: %.0f" % [
		day, String(season), pawns, wolves, tension
	]
	_push_entry(&"info", text, Vector2i(-1, -1))
	Audit.log("alerts_log", "day_ended logged: %s" % text)


## Boot catch-up: scan World.rooms for rooms exceeding the auto-roof cap and
## replay them through the translator. RoomDetector emits room_too_large during
## World init, before this CanvasLayer mounts; without this catch-up the cabin
## warning (always present at boot in the demo seed) never reaches the log.
func _catch_up_room_too_large() -> void:
	for id in World.rooms:
		var r = World.rooms[id]
		if r != null and r.tile_count() > Room.ROOM_AUTOROOF_CAP:
			_on_room_too_large(r.bounds.position, r.tile_count())


## Translates room_too_large → alert_added (warn).
func _on_room_too_large(top_left: Vector2i, cell_count: int) -> void:
	EventBus.alert_added.emit(
		&"warn",
		"Room too large to roof (%d tiles). Split with an interior wall." % cell_count,
		top_left,
	)
	Audit.log("alerts_log", "room_too_large translated: %d tiles at %s" % [cell_count, top_left])


## Translates no_stockpile_accepts → alert_added (warn).
func _on_no_stockpile_accepts(item_type: StringName, tile: Vector2i) -> void:
	EventBus.alert_added.emit(
		&"warn",
		"No stockpile accepts %s" % String(item_type),
		tile,
	)
	Audit.log("alerts_log", "no_stockpile_accepts translated: %s at %s" % [String(item_type), tile])


## Translates bill_blocked → alert_added (warn).
func _on_bill_blocked(recipe_label: String, reason: StringName, focus_tile: Vector2i) -> void:
	var reason_str: String = {
		&"missing_ingredient": "missing ingredient",
		&"skill_too_low":      "skill too low",
		&"no_workbench":       "no workbench reachable",
	}.get(reason, String(reason))
	EventBus.alert_added.emit(
		&"warn",
		"Bill blocked: %s — %s" % [recipe_label, reason_str],
		focus_tile,
	)
	Audit.log("alerts_log", "bill_blocked translated: %s — %s" % [recipe_label, reason_str])


# ── helpers ───────────────────────────────────────────────────────────────────

func _format_timestamp() -> String:
	if Clock == null:
		return ""
	return "Day %d, %s" % [Clock.current_day(), Clock.time_string()]


func _update_badge() -> void:
	if log_button == null:
		return
	if _unread_count > 0:
		log_button.text = "Log [%d]" % _unread_count
	else:
		log_button.text = "Log"


func _pan_to(tile: Vector2i) -> void:
	var cam = get_node_or_null("/root/Main/World/CameraRig")
	if cam == null:
		Audit.log("alerts_log", "pan_to: CameraRig not found")
		return
	if cam.has_method("pan_to_tile"):
		cam.pan_to_tile(tile)
	else:
		cam.position = Vector2(tile.x * 16 + 8, tile.y * 16 + 8)
	close()


func _unhandled_input(event: InputEvent) -> void:
	# L — toggle the alerts log.
	if event.is_action_pressed("open_log"):
		if _root != null and _root.visible:
			close()
		else:
			open()
		get_viewport().set_input_as_handled()
		return
	# Escape — close if open.
	if event.is_action_pressed("cancel") and _root != null and _root.visible:
		close()
		get_viewport().set_input_as_handled()
		return


func _on_backdrop_input(event: InputEvent) -> void:
	if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
		close()
	elif event is InputEventScreenTouch and event.pressed:
		close()
