extends Node
## In-game clock: sim-tick → day, hour, minute, darkness factor.
##
## Design (docs/architecture.md "Time / tick model"):
##   - 1 in-game day = TICKS_PER_DAY sim ticks
##       4800 ticks/day = 240 s real at 1× = 48 s at Fast (5×) = 20 s at Ultra (12×)
##   - 24 in-game hours per day → TICKS_PER_HOUR = 200
##   - TICKS_PER_MINUTE = 200 / 60 = 3 (integer floor; ~3.33 ticks per in-game min)
##
## Time-of-day phases (docs/design.md "Day/Night cycle"):
##   Night: 22:00–05:00
##   Dawn:  05:00–07:00 (2-hour smooth ramp, darkness 1.0 → 0.0)
##   Day:   07:00–19:00
##   Dusk:  19:00–22:00 (3-hour smooth ramp, darkness 0.0 → 1.0)
##
## darkness_factor() returns 0.0 (full daylight) to 1.0 (deepest night).
##
## Game starts at Day 1, 06:00 (mid-dawn — atmospheric, not full dark).
##
## Save seam: save_dict() / apply_dict() persist _start_offset_ticks so
## the time-of-day survives a session reload.

const TICKS_PER_DAY: int  = 4800
const TICKS_PER_HOUR: int = 200
## Integer floor of 200/60 ≈ 3.33. Minute display rounds down — fine for a clock.
const TICKS_PER_MINUTE: int = TICKS_PER_HOUR / 60

const START_HOUR: int    = 6
const HOURS_PER_DAY: int = 24

# Phase boundaries (in-game hours, inclusive lower bound).
const DAWN_START_HOUR: int = 5
const DAWN_END_HOUR: int   = 7
const DUSK_START_HOUR: int = 19
const DUSK_END_HOUR: int   = 22

## Internal: added to Sim.tick so we begin at START_HOUR rather than midnight.
var _start_offset_ticks: int = START_HOUR * TICKS_PER_HOUR

## Fired when the time-phase transitions (Night → Dawn, Dawn → Day, etc.).
signal phase_changed(phase: StringName)

const PHASE_NIGHT: StringName = &"night"
const PHASE_DAWN:  StringName = &"dawn"
const PHASE_DAY:   StringName = &"day"
const PHASE_DUSK:  StringName = &"dusk"

# Phase 12 — Season contracts (Agent A fills in current_season() + day_of_season()).
# Names locked here so Agent B + C can reference them without race conditions.
const SEASON_SPRING: StringName = &"spring"
const SEASON_SUMMER: StringName = &"summer"
const SEASON_AUTUMN: StringName = &"autumn"
const SEASON_WINTER: StringName = &"winter"
const SEASONS: Array[StringName] = [SEASON_SPRING, SEASON_SUMMER, SEASON_AUTUMN, SEASON_WINTER]
const DAYS_PER_SEASON: int = 12
const SEASONS_PER_YEAR: int = 4
const DAYS_PER_YEAR: int = DAYS_PER_SEASON * SEASONS_PER_YEAR  # 48

var _last_emitted_phase: StringName = &""
## Mirrors _last_emitted_phase — guards the season_changed emit against repeat fires.
var _last_emitted_season: StringName = &""


func _ready() -> void:
	EventBus.sim_tick.connect(_on_sim_tick)


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

## Current in-game day (1-indexed). Day 1 starts at boot.
func current_day() -> int:
	return 1 + (_offset_ticks() / TICKS_PER_DAY)


## Current hour 0..23.
func current_hour() -> int:
	return (_offset_ticks() / TICKS_PER_HOUR) % HOURS_PER_DAY


## Current minute 0..59.
func current_minute() -> int:
	var ticks_into_hour: int = _offset_ticks() % TICKS_PER_HOUR
	return (ticks_into_hour * 60) / TICKS_PER_HOUR


## Time of day as a 0..1 fraction (0.0 = midnight, 0.5 = noon).
func time_of_day_fraction() -> float:
	return float(_offset_ticks() % TICKS_PER_DAY) / float(TICKS_PER_DAY)


## Returns 0.0 (full daylight) to 1.0 (deepest night).
##
## Ramp shapes:
##   Day   [07:00–19:00] → 0.0 flat
##   Night [22:00–05:00] → 1.0 flat
##   Dawn  [05:00–07:00] → 1.0 ramps down to 0.0 linearly over 2 hours
##   Dusk  [19:00–22:00] → 0.0 ramps up to 1.0 linearly over 3 hours
func darkness_factor() -> float:
	var h: float = float(current_hour()) + float(current_minute()) / 60.0
	# Full daylight band.
	if h >= DAWN_END_HOUR and h < DUSK_START_HOUR:
		return 0.0
	# Full night band — handles the wrap (h >= 22 OR h < 5).
	if h >= DUSK_END_HOUR or h < DAWN_START_HOUR:
		return 1.0
	# Dawn ramp: 1.0 at 05:00 → 0.0 at 07:00
	if h < DAWN_END_HOUR:
		return 1.0 - (h - DAWN_START_HOUR) / float(DAWN_END_HOUR - DAWN_START_HOUR)
	# Dusk ramp: 0.0 at 19:00 → 1.0 at 22:00
	return (h - DUSK_START_HOUR) / float(DUSK_END_HOUR - DUSK_START_HOUR)


## Current phase as a StringName constant. Phase transitions emit phase_changed.
func current_phase() -> StringName:
	var h: int = current_hour()
	if h < DAWN_START_HOUR or h >= DUSK_END_HOUR:
		return PHASE_NIGHT
	if h < DAWN_END_HOUR:
		return PHASE_DAWN
	if h < DUSK_START_HOUR:
		return PHASE_DAY
	return PHASE_DUSK


## "HH:MM" formatted 24-hour time string.
func time_string() -> String:
	return "%02d:%02d" % [current_hour(), current_minute()]


# Phase 12 — Season public API (stub; Agent A wires emit + season_changed).

## Day index since game start (0-indexed, ignores 1-indexed display).
func day_index_from_start() -> int:
	return _offset_ticks() / TICKS_PER_DAY


## Current season index 0..3 (spring/summer/autumn/winter).
func current_season_index() -> int:
	return (day_index_from_start() / DAYS_PER_SEASON) % SEASONS_PER_YEAR


## Current season as a locked StringName constant.
func current_season() -> StringName:
	return SEASONS[current_season_index()]


## Day within the current season, 0..DAYS_PER_SEASON-1.
func day_of_season() -> int:
	return day_index_from_start() % DAYS_PER_SEASON


## Current year (1-indexed; Year 1 starts at boot).
func current_year() -> int:
	return 1 + day_index_from_start() / DAYS_PER_YEAR


# ── internal ─────────────────────────────────────────────────────────────────

func _offset_ticks() -> int:
	return _start_offset_ticks + Sim.tick


func _on_sim_tick(_n: int) -> void:
	var phase: StringName = current_phase()
	if phase != _last_emitted_phase:
		var prev_phase: StringName = _last_emitted_phase
		_last_emitted_phase = phase
		emit_signal("phase_changed", phase)
		Audit.log("clock", "phase → %s (day %d, %s)" % [phase, current_day(), time_string()])
		# Phase 17 — emit day_ended summary at the dusk→night boundary.
		# "Night" begins at DUSK_END_HOUR (22:00). This fires once per in-game day.
		# Listeners can use this for end-of-day recap UI, tension logging, etc.
		# Defensive: use get() for Storyteller/Weather/World fields that may not be
		# fully wired in all test contexts.
		if prev_phase == PHASE_DUSK and phase == PHASE_NIGHT:
			var summary: Dictionary = {
				"day":          current_day(),
				"weather":      Weather.get("current_weather") if Weather != null else &"unknown",
				"season":       current_season(),
				"pawns_alive":  World.pawns.size() if World != null else 0,
				"tension":      Storyteller.get("tension") if Storyteller != null else 0.0,
				"wolves_alive": World.wolves.size() if World != null else 0,
			}
			EventBus.day_ended.emit(summary)
			Audit.log("clock", "day_ended: day=%d season=%s pawns=%d tension=%.1f wolves=%d" % [
				summary["day"], summary["season"], summary["pawns_alive"],
				summary["tension"], summary["wolves_alive"]
			])

	var season: StringName = current_season()
	if season != _last_emitted_season:
		_last_emitted_season = season
		EventBus.season_changed.emit(season)
		Audit.log("clock", "season → %s (day %d/%d of season, year %d)" % [
			season, day_of_season() + 1, DAYS_PER_SEASON, current_year()
		])


# ── save / load ──────────────────────────────────────────────────────────────

func save_dict() -> Dictionary:
	return {"start_offset_ticks": _start_offset_ticks}


func apply_dict(d: Dictionary) -> void:
	_start_offset_ticks = int(d.get("start_offset_ticks", START_HOUR * TICKS_PER_HOUR))
