extends RefCounted
## Housing v2 P5 (docs/HOUSING_V2.md), server side - living together in Hearthvale:
##  - VISIT (house_visit {name}): warp to anyone's gate by character name (owners and tenants' houses are always
##    walkable); the first neighbour visit a day counts for the "visit" daily. DOORBELL (house_bell {plot}) at a gate:
##    the owner hears it wherever they are (chat line), and it lands in the house log.
##  - PICK (garden_pick {plot, k}): take 1 crop / flower from a ripe bed of SOMEONE ELSE'S garden. 5 picks a day per
##    character, 2 pickers per bed per planting, never twice the same bed; the owner's harvest is that much smaller
##    (min 1) and the pick lands in the house log ("Mina picked a Ruby Tomato").
##  - WEEKLY LANE TASKS: 3 tasks per lane per week (seeded), goals scale with the residents; every resident's daily
##    actions anywhere count (Home.progress -> contribute). When all 3 are done, each resident who helped (>= 5) claims
##    400 Home Coins + a Wishing Lantern Ticket at the Community Board (lane_claim). Stored in the community record.
##  - HOUSE PARTY (house_party): once a day the owner opens a 10-minute party at home (lights + balloons + music notes
##    on the plot); anyone on the plot can join the dance circle (house_dance: a "dance" pose slot round the party
##    centre): guests get 20 Home Coins once per party, the host 10 per guest (max 10).

const Life = preload("res://scripts/server/life.gd")
const Housing = preload("res://scripts/server/housing.gd")


static func S() -> Dictionary:
	return Life.data().get("social", {})


static func Home():
	return load("res://scripts/server/home.gd")


static func ensure(c) -> void:
	if not c.life.has("social") or not (c.life["social"] is Dictionary):
		c.life["social"] = {"day": 0, "picks": 0, "visit": 0, "party_day": 0}


static func from_save(d: Dictionary, out: Dictionary) -> void:
	var s = d.get("social", {})
	if s is Dictionary and not s.is_empty():
		out["social"] = {"day": int(s.get("day", 0)), "picks": int(s.get("picks", 0)), "visit": int(s.get("visit", 0)), "party_day": int(s.get("party_day", 0))}


static func today_rec(c) -> Dictionary:
	ensure(c)
	var s: Dictionary = c.life["social"]
	var d: int = Home().today()
	if int(s.get("day", 0)) != d:
		s["day"] = d
		s["picks"] = 0
		s["visit"] = 0
	return s


static func log_add(r: Dictionary, text: String) -> void:
	if not r.has("log") or not (r["log"] is Array):
		r["log"] = []
	(r["log"] as Array).push_front({"t": int(Life.unix()), "x": text})
	while (r["log"] as Array).size() > int(S().get("log_max", 12)):
		(r["log"] as Array).pop_back()


## [comm, plot] of a character's house by name ([] = none).
static func house_of(name: String) -> Array:
	var want := name.strip_edges().to_lower()
	for m in Housing.maps():
		var plots: Array = Housing.comm(str(m)).get("plots", [])
		for i in plots.size():
			if str(plots[i].get("owner", "")).to_lower() == want and want != "":
				return [str(m), i]
	return []


# ---------------------------------------------------------------- visits + doorbell

static func visit(pe, name: String) -> Dictionary:
	var h := house_of(name)
	if h.is_empty():
		return {"ok": false, "err": "%s has no house in Hearthvale." % name}
	if str(pe.ch.char_name).to_lower() == name.strip_edges().to_lower():
		return Housing.command(pe, "house_go", {})
	var g := Housing.to_world(int(h[1]), Vector2(0, Housing.plot_size() / 2.0 + 1.5))
	if pe.map != null and pe.map.map_id == str(h[0]):
		pe.server.teleport(pe, Vector3(g.x, 0, g.y))
	else:
		pe.server.warp(pe, str(h[0]), Vector3(g.x, 0, g.y))
	var s := today_rec(pe.ch)
	if int(s["visit"]) == 0:
		s["visit"] = 1
		Home().progress(pe.ch, "visit")
	pe.ch.msg("Visiting %s's house.  ·  ไปเยี่ยมบ้าน" % name, Color(0.8, 0.95, 1))
	return {"ok": true, "comm": str(h[0]), "plot": int(h[1])}


static var _bell_t: Dictionary = {}


static func bell(pe, i: int) -> Dictionary:
	if pe.map == null or not Housing.is_community(pe.map.map_id):
		return {"ok": false, "err": ""}
	var comm: String = pe.map.map_id
	var r := Housing.plot_rec(comm, i)
	var owner := str(r.get("owner", ""))
	if owner == "":
		return {"ok": false, "err": "Nobody lives there yet."}
	var gate := Housing.to_world(i, Vector2(0, Housing.plot_size() / 2.0 - 0.4))
	if pe.flat_dist(Vector3(gate.x, 0, gate.y)) > 4.0:
		return {"ok": false, "err": "Ring the bell at the gate."}
	var k := "%s:%d:%s" % [comm, i, str(pe.ch.char_name)]
	if Life.unix() - float(_bell_t.get(k, -999.0)) < float(S().get("bell_cooldown_s", 20)):
		return {"ok": false, "err": "Give them a moment to answer..."}
	_bell_t[k] = Life.unix()
	var who := str(pe.ch.char_name)
	if who != owner:
		log_add(r, "%s rang your doorbell" % who)
		var op = pe.server.player_named(owner) if pe.server != null and pe.server.has_method("player_named") else null
		if op != null:
			op.ch.msg("Ding-dong! %s is at your gate (Hearthvale Lane %s, No. %d).  ·  มีคนกดกริ่งบ้าน" % [who, comm.get_slice("_", 1), i + 1], Color(1, 0.9, 0.6))
		Housing.save_store()
	pe._fx("ring", {"at": Vector3(gate.x, 1.2, gate.y), "r": 0.8, "color": Color(1, 0.9, 0.5), "time": 0.4})
	pe.ch.msg("Ding-dong!  ·  กริ๊ง" + ("" if pe.server.player_named(owner) != null else "  (nobody home - they will see it later)"), Color(1, 0.9, 0.6))
	return {"ok": true, "home": pe.server.player_named(owner) != null}


# ---------------------------------------------------------------- picking from neighbours' gardens

static func pick_block(c, r: Dictionary, k: int) -> String:
	var LifeHome = load("res://scripts/server/life_home.gd")
	var g: Array = r.get("garden", [])
	if k < 0 or k >= g.size() or not (g[k] is Dictionary) or str(g[k].get("seed", "")) == "":
		return "Nothing grows there."
	if str(r.get("owner", "")) == "" or str(r.get("owner", "")) == str(c.char_name):
		return "Pick from your neighbours' gardens - harvest your own!"
	if LifeHome.growth_rec(r, k) < 1.0:
		return "Not ripe yet."
	var pk: Array = g[k].get("pk", [])
	if pk.has(str(c.char_name)):
		return "You already picked from this bed."
	if pk.size() >= int(S().get("pickers_per_bed", 2)):
		return "Others got here first - leave the rest for the owner."
	if int(today_rec(c)["picks"]) >= int(S().get("picks_per_day", 5)):
		return "That is enough picking for today.  ·  เก็บครบแล้ววันนี้"
	return ""


static func pick(pe, i: int, k: int) -> Dictionary:
	var c = pe.ch
	if pe.map == null or not Housing.is_community(pe.map.map_id):
		return {"ok": false, "err": ""}
	var comm: String = pe.map.map_id
	var r := Housing.plot_rec(comm, i)
	var err := pick_block(c, r, k)
	if err != "":
		return {"ok": false, "err": err}
	var LifeHome = load("res://scripts/server/life_home.gd")
	if pe.flat_dist(LifeHome.garden_world(i, int(r.get("lv", 1)), k)) > float(Life.housing().get("plot_reach", 3.0)) + 1.1:
		return {"ok": false, "err": "Walk up to the bed first."}
	var e: Dictionary = r["garden"][k]
	var crop := str(LifeHome.crop_of(str(e["seed"])).get("crop", ""))
	if not c.can_carry(crop, 1):
		return {"ok": false, "err": "Your bag is too heavy."}
	if not e.has("pk"):
		e["pk"] = []
	(e["pk"] as Array).append(str(c.char_name))
	c.add_item(crop, 1)
	today_rec(c)["picks"] = int(today_rec(c)["picks"]) + 1
	log_add(r, "%s picked a %s" % [str(c.char_name), GameData.item_name(crop)])
	c.msg("Picked a %s from %s's garden (%d/%d today).  ·  เก็บผลผลิตเพื่อนบ้าน" % [GameData.item_name(crop), str(r["owner"]), int(today_rec(c)["picks"]),
		int(S().get("picks_per_day", 5))], Color(0.75, 1, 0.6))
	Housing.changed(pe.server, comm, i)
	c.changed.emit()
	return {"ok": true, "crop": crop}


# ---------------------------------------------------------------- weekly lane tasks

static func week() -> int:
	return int(Life.unix() / 604800.0)


static func residents(comm: String) -> int:
	var n := 0
	for p in Housing.comm(comm).get("plots", []):
		if str(p.get("owner", "")) != "":
			n += 1
	return n


## The lane's tasks of this week (created on first use): {"wk", "tasks": [{"id", "n", "goal"}], "contrib": {name: n}, "claimed": [names]}.
static func lane_week(comm: String) -> Dictionary:
	var cm := Housing.comm(comm)
	if cm.is_empty():
		return {}
	var w = cm.get("week", {})
	if not (w is Dictionary) or int(w.get("wk", -1)) != week():
		var W: Dictionary = S().get("week", {})
		var pool: Array = (W.get("tasks", []) as Array).duplicate()
		var rng := RandomNumberGenerator.new()
		rng.seed = hash("%s:%d:week" % [comm, week()])
		var tasks: Array = []
		var nres := maxi(3, residents(comm))
		for t in mini(int(W.get("count", 3)), pool.size()):
			var j := rng.randi_range(0, pool.size() - 1)
			var e: Array = pool[j]
			pool.remove_at(j)
			tasks.append({"id": str(e[0]), "n": 0, "goal": maxi(int(e[4]), int(e[3]) * nres)})
		w = {"wk": week(), "tasks": tasks, "contrib": {}, "claimed": []}
		cm["week"] = w
	return w


static func task_name(id: String) -> Array:
	for e in S().get("week", {}).get("tasks", []):
		if str(e[0]) == id:
			return [str(e[1]), str(e[2])]
	return [id, id]


static func week_done(w: Dictionary) -> bool:
	if w.is_empty():
		return false
	for t in w.get("tasks", []):
		if int(t["n"]) < int(t["goal"]):
			return false
	return true


## Home.progress hook: the action counts for the character's own lane.
static func contribute(c, key: String, n: int) -> void:
	if c == null or not (c.life.get("house", null) is Dictionary):
		return
	var comm := str(c.life["house"].get("comm", ""))
	if comm == "" or not Housing.is_community(comm):
		return
	var w := lane_week(comm)
	var hit := false
	for t in w.get("tasks", []):
		if str(t["id"]) == key and int(t["n"]) < int(t["goal"]):
			t["n"] = mini(int(t["goal"]), int(t["n"]) + n)
			hit = true
	if hit:
		var nm := str(c.char_name)
		w["contrib"][nm] = int(w["contrib"].get(nm, 0)) + n
		Housing.save_store()


static func claim(pe) -> Dictionary:
	var c = pe.ch
	var comm := str(c.life.get("house", {}).get("comm", ""))
	if comm == "":
		return {"ok": false, "err": "Only residents of a lane get its weekly reward."}
	var w := lane_week(comm)
	if not week_done(w):
		return {"ok": false, "err": "The lane has not finished this week's tasks yet."}
	var nm := str(c.char_name)
	if (w["claimed"] as Array).has(nm):
		return {"ok": false, "err": "You already claimed this week's reward."}
	var W: Dictionary = S().get("week", {})
	if int(w["contrib"].get(nm, 0)) < int(W.get("min_contrib", 5)):
		return {"ok": false, "err": "Help your lane a little more first (%d / %d)." % [int(w["contrib"].get(nm, 0)), int(W.get("min_contrib", 5))]}
	(w["claimed"] as Array).append(nm)
	Home().add_coins(c, int(W.get("reward_coins", 400)), "weekly lane reward")
	for it in W.get("reward_items", {}):
		c.add_item(str(it), int(W["reward_items"][it]))
	c.msg("Weekly lane reward claimed - thank you, neighbour!  ·  รับรางวัลประจำสัปดาห์แล้ว", Color(1, 0.9, 0.5))
	Housing.save_store()
	c.changed.emit()
	return {"ok": true}


## lane_week command: the data for the Community window (own lane, or the lane you stand on).
static func week_info(pe) -> Dictionary:
	var c = pe.ch
	var comm := str(c.life.get("house", {}).get("comm", ""))
	if comm == "" and pe.map != null and Housing.is_community(pe.map.map_id):
		comm = pe.map.map_id
	if comm == "":
		return {"ok": true, "comm": "", "tasks": []}
	var w := lane_week(comm)
	var left := int((week() + 1) * 604800 - Life.unix())
	var houses: Array = []
	for m in Housing.maps():
		var plots: Array = Housing.comm(str(m)).get("plots", [])
		for i in plots.size():
			if str(plots[i].get("owner", "")) != "":
				houses.append({"name": str(plots[i]["owner"]), "comm": str(m), "plot": i, "lv": int(plots[i].get("lv", 1)),
					"online": pe.server != null and pe.server.has_method("player_named") and pe.server.player_named(str(plots[i]["owner"])) != null,
					"party": party_on(plots[i])})
	var own := Housing.own_rec(c)
	return {"ok": true, "comm": comm, "tasks": w.get("tasks", []), "contrib": int(w["contrib"].get(str(c.char_name), 0)),
		"done": week_done(w), "claimed": (w["claimed"] as Array).has(str(c.char_name)), "left_s": left,
		"min": int(S().get("week", {}).get("min_contrib", 5)), "houses": houses, "log": own.get("log", [])}


# ---------------------------------------------------------------- house parties

static func party_on(r: Dictionary) -> bool:
	var p = r.get("party", {})
	return p is Dictionary and float(p.get("until", 0)) > Life.unix()


static func party_start(pe) -> Dictionary:
	var c = pe.ch
	var r := Housing.own_rec(c)
	if r.is_empty():
		return {"ok": false, "err": "You have no house yet."}
	var comm := str(c.life["house"]["comm"])
	var i := int(c.life["house"]["plot"])
	if pe.map == null or pe.map.map_id != comm or not Housing.on_plot(i, pe.global_position):
		return {"ok": false, "err": "Throw the party at your own house."}
	if party_on(r):
		return {"ok": false, "err": "The party is already on!"}
	var s := today_rec(c)
	if int(s.get("party_day", 0)) == int(Home().today()):
		return {"ok": false, "err": "One party a day - the neighbours need their sleep!"}
	s["party_day"] = int(Home().today())
	var P: Dictionary = S().get("party", {})
	var hr := Housing.house_rect(int(r.get("lv", 1)))
	r["party"] = {"until": Life.unix() + float(P.get("dur_s", 600)), "host": str(c.char_name), "guests": [],
		"at": [0.0, hr.end.y + 3.2]}
	log_add(r, "You threw a party")
	for p in pe.map.players():
		p.ch.msg("%s is throwing a house party at No. %d - come and dance!  ·  ปาร์ตี้บ้าน!" % [str(c.char_name), i + 1], Color(1, 0.75, 0.95))
	Housing.changed(pe.server, comm, i)
	return {"ok": true}


## Dance slots round the party centre (plot-local).
static func dance_spot(r: Dictionary, slot: int) -> Vector2:
	var c: Array = r["party"].get("at", [0, 0])
	var rad := float(S().get("party", {}).get("radius", 3.2))
	var a := slot * TAU / 8.0
	return Vector2(float(c[0]) + cos(a) * rad, float(c[1]) + sin(a) * rad)


static func dance(pe, i: int) -> Dictionary:
	var c = pe.ch
	if pe.map == null or not Housing.is_community(pe.map.map_id):
		return {"ok": false, "err": ""}
	var comm: String = pe.map.map_id
	var r := Housing.plot_rec(comm, i)
	if not party_on(r):
		return {"ok": false, "err": "No party here right now."}
	if not Housing.on_plot(i, pe.global_position):
		return {"ok": false, "err": "Join the party first."}
	var H = Home()
	var taken := {}
	for k in H.poses:
		var ps: Dictionary = H.poses[k]
		if str(ps["comm"]) == comm and int(ps["plot"]) == i and str(ps["pose"]) == "dance":
			taken[int(ps["slot"])] = true
	var slot := -1
	for k in 8:
		if not taken.has(k):
			slot = k
			break
	if slot < 0:
		return {"ok": false, "err": "The dance floor is full!"}
	var l := dance_spot(r, slot)
	var w := Housing.to_world(i, l)
	var at := Vector3(w.x, 0, w.y)
	var cen: Array = r["party"]["at"]
	var cw := Housing.to_world(i, Vector2(float(cen[0]), float(cen[1])))
	var yaw := atan2(cw.x - w.x, cw.y - w.y)
	pe._clear_goals()
	pe.attack_target = null
	pe.global_position = at
	pe.sitting = true
	pe.collision_mask = 0
	H.poses[pe.get_instance_id()] = {"comm": comm, "plot": i, "i": -1, "slot": slot, "pose": "dance", "at": at, "stand": at + Vector3(sin(yaw), 0, cos(yaw)) * -0.8, "yaw": yaw}
	pe.server.emit_ev({"t": "life", "k": "pose", "id": pe.id, "pose": "dance", "at": at, "yaw": yaw, "h": 0.0, "_m": comm})
	var guests: Array = r["party"]["guests"]
	var nm := str(c.char_name)
	var P: Dictionary = S().get("party", {})
	if nm != str(r["party"]["host"]) and not guests.has(nm):
		guests.append(nm)
		H.add_coins(c, int(P.get("guest_coins", 20)), "party guest")
		H.progress(c, "dance")
		if guests.size() <= int(P.get("host_cap", 10)):
			var hp = pe.server.player_named(str(r["party"]["host"])) if pe.server.has_method("player_named") else null
			if hp != null:
				H.add_coins(hp.ch, int(P.get("host_coins_per_guest", 10)), "%s joined your party" % nm)
		log_add(r, "%s danced at your party" % nm)
		Housing.save_store()
	elif nm == str(r["party"]["host"]):
		H.progress(c, "dance")
	c.msg("You dance!  ·  เต้นกัน!", Color(1, 0.8, 0.95))
	return {"ok": true, "slot": slot}


# ---------------------------------------------------------------- commands

static func command(pe, t: String, cmd: Dictionary) -> Dictionary:
	match t:
		"house_visit":
			return visit(pe, str(cmd.get("name", "")))
		"house_bell":
			return bell(pe, int(cmd.get("plot", -1)))
		"garden_pick":
			return pick(pe, int(cmd.get("plot", -1)), int(cmd.get("k", -1)))
		"lane_week":
			return week_info(pe)
		"lane_claim":
			return claim(pe)
		"house_party":
			return party_start(pe)
		"house_dance":
			return dance(pe, int(cmd.get("plot", -1)))
	return {"ok": false, "err": "unknown social command"}
