/**
 * EQEmulator: Everquest Server Emulator
 * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY except by those people which sell it, which
 * are required to give you total support for your newly bought product;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 */

#include "dynamic_zone.h"
#include "client.h"
#include "expedition_request.h"
#include "string_ids.h"
#include "worldserver.h"
#include "../common/repositories/character_expedition_lockouts_repository.h"
#include "../common/repositories/dynamic_zone_lockouts_repository.h"
#include <cereal/types/utility.hpp>

extern WorldServer worldserver;

// various expeditions use these strings when locking
constexpr char LockClose[] = "Your expedition is nearing its close. You cannot bring any additional people into your expedition at this time.";
constexpr char LockBegin[] = "The trial has begun. You cannot bring any additional people into your expedition at this time.";

DynamicZone::DynamicZone(uint32_t zone_id, uint32_t version, uint32_t duration, DynamicZoneType type)
{
	m_zone_id = zone_id;
	m_zone_version = version;
	m_duration = std::chrono::seconds(duration);
	m_type = type;
}

Database& DynamicZone::GetDatabase()
{
	return database;
}

bool DynamicZone::SendServerPacket(ServerPacket* packet)
{
	return worldserver.SendPacket(packet);
}

uint16_t DynamicZone::GetCurrentInstanceID() const
{
	return zone ? static_cast<uint16_t>(zone->GetInstanceID()) : 0;
}

uint16_t DynamicZone::GetCurrentZoneID() const
{
	return zone ? static_cast<uint16_t>(zone->GetZoneID()) : 0;
}

DynamicZone* DynamicZone::TryCreate(Client& client, DynamicZone& dzinfo, bool silent)
{
	// only expedition types are currently created in zone
	if (!zone || dzinfo.GetID() != 0 || !dzinfo.IsExpedition())
	{
		return nullptr;
	}

	// request parses leader, members list, and lockouts while validating
	ExpeditionRequest request(dzinfo, client, silent);
	if (!request.Validate())
	{
		LogExpeditions("[{}] request by [{}] denied", dzinfo.GetName(), client.GetName());
		return nullptr;
	}

	dzinfo.SetLeader({ request.GetLeaderID(), request.GetLeaderName(), DynamicZoneMemberStatus::Online });

	// this creates a new dz instance and saves it to both db and cache
	uint32_t dz_id = dzinfo.Create();
	if (dz_id == 0)
	{
		// live uses this message when trying to enter an instance that isn't ready
		// for now we can use it as a client error message if instance creation fails
		client.MessageString(Chat::Red, DZ_PREVENT_ENTERING);
		LogDynamicZones("Failed to create dynamic zone for zone [{}]", dzinfo.GetZoneID());
		return nullptr;
	}

	auto [it, ok] = zone->dynamic_zone_cache.try_emplace(dz_id, std::make_unique<DynamicZone>(dzinfo));

	DynamicZone* dz = it->second.get();
	dz->SaveMembers(request.GetMembers());
	dz->SaveLockouts(request.GetLockouts());

	dz->SendLeaderMessage(request.GetLeaderClient(), Chat::System, DZ_AVAILABLE, { dz->GetName() });
	if (dz->GetMemberCount() < request.GetMembers().size())
	{
		dz->SendLeaderMessage(request.GetLeaderClient(), Chat::System, fmt::format(DzNotAllAdded,
			request.IsRaid() ? "raid" : "group", "expedition", dz->GetMaxPlayers(), request.GetMembers().size()));
	}

	// world must be notified before we request async member updates
	auto pack = dz->CreateServerPacket(zone->GetZoneID(), zone->GetInstanceID());
	worldserver.SendPacket(pack.get());

	dz->UpdateMembers();

	LogDynamicZones("Created new dz [{}] for zone [{}]", dz_id, dz->GetZoneID());

	return dz;
}

void DynamicZone::CacheNewDynamicZone(ServerPacket* pack)
{
	auto buf = reinterpret_cast<ServerDzCreate_Struct*>(pack->pBuffer);

	// caching new dz created in world or another zone (has member statuses set by world)
	auto [it, ok] = zone->dynamic_zone_cache.try_emplace(buf->dz_id, std::make_unique<DynamicZone>());
	it->second->Unserialize({ buf->cereal_data, buf->cereal_size });
	it->second->UpdateMembers();

	LogDynamicZones("Cached new dynamic zone [{}]", buf->dz_id);
}

void DynamicZone::CacheAllFromDatabase()
{
	if (!zone)
	{
		return;
	}

	BenchTimer bench;

	auto dzs = DynamicZonesRepository::AllWithInstanceNotExpired(database);
	auto members = DynamicZoneMembersRepository::AllWithNames(database);
	auto lockouts = DynamicZoneLockoutsRepository::All(database);

	zone->dynamic_zone_cache.clear();
	zone->dynamic_zone_cache.reserve(dzs.size());

	for (auto& entry : dzs)
	{
		uint32_t dz_id = entry.id;
		auto dz = std::make_unique<DynamicZone>(std::move(entry));

		for (auto& member : members)
		{
			if (member.dynamic_zone_id == dz_id)
			{
				dz->AddMemberFromRepositoryResult(std::move(member));
			}
		}

		for (auto& lockout : lockouts)
		{
			if (lockout.dynamic_zone_id == dz->GetID())
			{
				dz->m_lockouts.emplace_back(dz->GetName(), std::move(lockout));
			}
		}

		zone->dynamic_zone_cache.emplace(dz_id, std::move(dz));
	}

	if (!zone->dynamic_zone_cache.empty())
	{
		RequestMemberStatuses();
	}

	LogInfo("Loaded [{}] dynamic zone(s)", Strings::Commify(zone->dynamic_zone_cache.size()));
	LogDynamicZones("Caching [{}] dynamic zone(s) took [{}s]", zone->dynamic_zone_cache.size(), bench.elapsed());
}

void DynamicZone::RequestMemberStatuses()
{
	ServerPacket pack(ServerOP_DzGetBulkMemberStatuses, sizeof(ServerDzCerealData_Struct));
	auto buf = reinterpret_cast<ServerDzCerealData_Struct*>(pack.pBuffer);
	buf->zone_id = static_cast<uint16_t>(zone->GetZoneID());
	buf->inst_id = static_cast<uint16_t>(zone->GetInstanceID());

	worldserver.SendPacket(&pack);
}

template <typename T>
DynamicZone* FindDynamicZone(T pred)
{
	if (zone)
	{
		for (const auto& [id_, dz] : zone->dynamic_zone_cache)
		{
			if (pred(*dz.get()))
			{
				return dz.get();
			}
		}
	}
	return nullptr;
}

DynamicZone* DynamicZone::FindDynamicZoneByID(uint32_t dz_id, DynamicZoneType type)
{
	if (zone)
	{
		auto it = zone->dynamic_zone_cache.find(dz_id);
		if (it != zone->dynamic_zone_cache.end() && (type == DynamicZoneType::None || it->second->GetType() == type))
		{
			return it->second.get();
		}
	}
	return nullptr;
}

DynamicZone* DynamicZone::FindExpeditionByCharacter(uint32_t char_id)
{
	return FindDynamicZone([&](const DynamicZone& dz) { return dz.IsExpedition() && dz.HasMember(char_id); });
}

DynamicZone* DynamicZone::FindExpeditionByZone(uint32_t zone_id, uint32_t instance_id)
{
	return FindDynamicZone([&](const DynamicZone& dz) { return dz.IsExpedition() && dz.IsSameDz(zone_id, instance_id); });
}

void DynamicZone::StartAllClientRemovalTimers()
{
	for (const auto& client_iter : entity_list.GetClientList())
	{
		if (client_iter.second)
		{
			client_iter.second->SetDzRemovalTimer(true);
		}
	}
}

bool DynamicZone::IsCurrentZoneDz() const
{
	return zone && zone->GetInstanceID() != 0 && zone->GetInstanceID() == GetInstanceID();
}

void DynamicZone::SetSecondsRemaining(uint32_t seconds_remaining)
{
	// async
	constexpr uint32_t pack_size = sizeof(ServerDzSetDuration_Struct);
	auto pack = std::make_unique<ServerPacket>(ServerOP_DzSetSecondsRemaining, pack_size);
	auto buf = reinterpret_cast<ServerDzSetDuration_Struct*>(pack->pBuffer);
	buf->dz_id = GetID();
	buf->seconds = seconds_remaining;
	worldserver.SendPacket(pack.get());
}

void DynamicZone::SetUpdatedDuration(uint32_t new_duration)
{
	// preserves original start time, just modifies duration and expire time
	m_duration = std::chrono::seconds(new_duration);
	m_expire_time = m_start_time + m_duration;

	LogDynamicZones("Updated dz [{}] zone [{}]:[{}] seconds remaining: [{}]",
		m_id, m_zone_id, m_instance_id, GetSecondsRemaining());

	if (zone && IsCurrentZoneDz())
	{
		zone->SetInstanceTimer(GetSecondsRemaining());
	}
}

void DynamicZone::SendClientInvite(Client* client, const std::string& inviter, const std::string& swap_name)
{
	if (!client)
	{
		return;
	}

	LogExpeditions("Invite [{}] to [{}] by [{}] swap [{}]", client->GetName(), GetID(), inviter, swap_name);

	client->SetPendingDzInvite({ GetID(), inviter, swap_name });
	client->MessageString(Chat::System, DZ_INVITED, GetLeaderName().c_str(), GetName().c_str());

	// live (as of March 11 2020 patch) warns for lockouts added during current
	// expedition that client would receive upon entering (sent in invite packet)
	std::string events;
	for (const auto& lockout : m_lockouts)
	{
		// live doesn't issue a warning for the dz's replay timer
		if (!lockout.IsReplay() && !lockout.IsExpired() && lockout.IsUUID(GetUUID()) &&
		    !client->HasDzLockout(GetName(), lockout.Event()))
		{
			auto time = lockout.GetTimeRemainingStrs();
			events += fmt::format("\n{} - {}D:{}H:{}M", lockout.Event(), time.days, time.hours, time.mins);
		}
	}

	if (!events.empty())
	{
		client->SendColoredText(Chat::System, fmt::format(
			"Warning! You will be given replay timers for the following events if you enter {}:{}", GetName(), events));
	}

	auto outapp = CreateInvitePacket(inviter, swap_name).release();
	client->FastQueuePacket(&outapp);
}

bool DynamicZone::ConfirmLeaderCommand(Client* client)
{
	if (!client)
	{
		return false;
	}

	if (!GetLeader().IsValid())
	{
		client->MessageString(Chat::Red, DZ_NO_LEADER_INFO); // unconfirmed message
		return false;
	}

	if (GetLeaderID() != client->CharacterID())
	{
		client->MessageString(Chat::System, DZ_NOT_LEADER, GetLeaderName().c_str());
		return false;
	}

	return true;
}

void DynamicZone::SendLeaderMessage(Client* leader, uint16_t type, const std::string& msg)
{
	Client::SendCrossZoneMessage(leader, GetLeaderName(), type, msg);
}

void DynamicZone::SendLeaderMessage(Client* leader, uint16_t type, uint32_t str_id, std::initializer_list<std::string> args)
{
	Client::SendCrossZoneMessageString(leader, GetLeaderName(), type, str_id, args);
}

bool DynamicZone::ProcessAddConflicts(Client* leader, Client* client, bool swapping)
{
	if (!client) // a null leader client is handled by SendLeaderMessage fallback
	{
		return true;
	}

	bool has_conflict = false;

	if (IsCurrentZoneDz())
	{
		SendLeaderMessage(leader, Chat::Red, DZADD_LEAVE_ZONE, { client->GetName() });
		has_conflict = true;
	}

	auto dz_id = client->GetExpeditionID();
	if (dz_id)
	{
		int string_id = dz_id == GetID() ? DZADD_ALREADY_PART : DZADD_ALREADY_OTHER;
		SendLeaderMessage(leader, Chat::Red, string_id, { client->GetName() });
		has_conflict = true;
	}

	// check any extra event lockouts for this expedition that the client has and expedition doesn't
	auto lockouts = client->GetDzLockouts(GetName());
	for (const auto& lockout : lockouts)
	{
		// client with a replay lockout is allowed only if the replay timer was from this expedition
		if (lockout.IsReplay() && lockout.UUID() != m_uuid)
		{
			has_conflict = true;

			auto time = lockout.GetTimeRemainingStrs();
			SendLeaderMessage(leader, Chat::Red, DZADD_REPLAY_TIMER, { client->GetName(), time.days, time.hours, time.mins });
		}
		else if (!lockout.IsReplay() && !HasLockout(lockout.Event()))
		{
			has_conflict = true;

			auto time = lockout.GetTimeRemainingStrs();
			SendLeaderMessage(leader, Chat::Red, DZADD_EVENT_TIMER, { client->GetName(), lockout.Event(), time.days, time.hours, time.mins });
		}
	}

	// member swapping integrity is handled by invite response
	if (!swapping)
	{
		auto member_count = GetDatabaseMemberCount();
		if (member_count == 0)
		{
			has_conflict = true;
		}
		else if (member_count >= GetMaxPlayers())
		{
			SendLeaderMessage(leader, Chat::Red, DZADD_EXCEED_MAX, { fmt::format_int(GetMaxPlayers()).str() });
			has_conflict = true;
		}
	}

	auto invite_id = client->GetPendingDzInviteID();
	if (invite_id)
	{
		int string_id = invite_id == GetID() ? DZADD_PENDING : DZADD_PENDING_OTHER;
		SendLeaderMessage(leader, Chat::Red, string_id, { client->GetName() });
		has_conflict = true;
	}

	return has_conflict;
}

void DynamicZone::TryAddClient(Client* client, const std::string& inviter, const std::string& swap_name, Client* leader)
{
	if (!client)
	{
		return;
	}

	LogExpeditions("Adding [{}] to [{}] by [{}] swap [{}]", client->GetName(), GetID(), inviter, swap_name);

	// null leader client handled by ProcessAddConflicts/SendLeaderMessage fallbacks
	if (!leader)
	{
		leader = entity_list.GetClientByName(inviter.c_str());
	}

	bool has_conflicts = ProcessAddConflicts(leader, client, !swap_name.empty());
	if (!has_conflicts)
	{
		// live uses the original unsanitized input string in invite messages
		uint32_t string_id = swap_name.empty() ? DZADD_INVITE : DZSWAP_INVITE;
		SendLeaderMessage(leader, Chat::Yellow, string_id, { client->GetName() });
		SendClientInvite(client, inviter, swap_name);
	}
	else if (swap_name.empty()) // swap command doesn't result in this message
	{
		SendLeaderMessage(leader, Chat::Red, DZADD_INVITE_FAIL, { client->GetName() });
	}
}

void DynamicZone::DzAddPlayer(Client* client, const std::string& add_name, const std::string& swap_name)
{
	if (!client || !ConfirmLeaderCommand(client))
	{
		return;
	}

	bool invite_failed = false;

	if (IsLocked())
	{
		client->MessageString(Chat::Red, DZADD_NOT_ALLOWING);
		invite_failed = true;
	}
	else if (add_name.empty())
	{
		client->MessageString(Chat::Red, DZADD_NOT_ONLINE, add_name.c_str());
		invite_failed = true;
	}
	else
	{
		auto member_data = GetMemberData(add_name);
		if (member_data.IsValid())
		{
			// live prioritizes offline message before already a member message
			if (member_data.status == DynamicZoneMemberStatus::Offline)
			{
				client->MessageString(Chat::Red, DZADD_NOT_ONLINE, add_name.c_str());
			}
			else
			{
				client->MessageString(Chat::Red, DZADD_ALREADY_PART, add_name.c_str());
			}
			invite_failed = true;
		}
	}

	if (invite_failed)
	{
		client->MessageString(Chat::Red, DZADD_INVITE_FAIL, FormatName(add_name).c_str());
		return;
	}

	if (Client* add_client = entity_list.GetClientByName(add_name.c_str()))
	{
		// client is online in this zone
		TryAddClient(add_client, client->GetName(), swap_name, client);
	}
	else
	{
		// forward to world to check if client is online and perform cross-zone invite
		SendWorldPlayerInvite(client->GetName(), swap_name, FormatName(add_name));
	}
}

void DynamicZone::DzAddPlayerContinue(std::string inviter, std::string add_name, std::string swap_name)
{
	// continuing expedition invite from leader in another zone
	if (Client* add_client = entity_list.GetClientByName(add_name.c_str()))
	{
		TryAddClient(add_client, inviter, swap_name);
	}
}

void DynamicZone::DzInviteResponse(Client* client, bool accepted, const std::string& swap_name)
{
	if (!client)
	{
		return;
	}

	LogExpeditions("[{}] accepted invite [{}] swap [{}]", client->GetName(), accepted, swap_name);

	// a null leader client is handled by SendLeaderMessage fallbacks
	// note current leader receives invite reply messages (if leader changed)
	Client* leader = entity_list.GetClientByCharID(GetLeaderID());

	if (!accepted)
	{
		SendLeaderMessage(leader, Chat::Red, DZ_INVITE_DECLINED, { client->GetName() });
		return;
	}

	bool is_swap = !swap_name.empty();
	bool has_conflicts = IsLocked();

	if (IsLocked())
	{
		SendLeaderMessage(leader, Chat::Red, DZADD_NOT_ALLOWING);
	}
	else
	{
		has_conflicts = ProcessAddConflicts(leader, client, is_swap);
	}

	// error if swapping and character was already removed before the accept
	if (is_swap)
	{
		auto swap_member = GetMemberData(swap_name);
		if (!swap_member.IsValid() || !HasDatabaseMember(swap_member.id))
		{
			has_conflicts = true;
		}
	}

	if (has_conflicts)
	{
		SendLeaderMessage(leader, Chat::Red, DZ_INVITE_ERROR, { client->GetName() });
	}
	else
	{
		SendLeaderMessage(leader, Chat::Yellow, DZ_INVITE_ACCEPTED, { client->GetName() });

		// replay timers are optionally added to new members on join with fresh expire time
		if (m_add_replay)
		{
			auto it = std::ranges::find_if(m_lockouts, [&](const auto& l) { return l.IsReplay(); });
			if (it != m_lockouts.end() && it->IsUUID(GetUUID()) && !client->HasDzLockout(GetName(), DzLockout::ReplayTimer))
			{
				DzLockout replay_timer = *it; // copy
				replay_timer.Reset();
				client->AddDzLockout(replay_timer, true);
			}
		}

		DynamicZoneMember add_member(client->CharacterID(), client->GetName(), DynamicZoneMemberStatus::Online);

		bool success = is_swap ? SwapMember(add_member, swap_name) : AddMember(add_member);
		if (success)
		{
			SendLeaderMessage(leader, Chat::Yellow, DZ_ADDED, { client->GetName(), GetName() });
		}
	}
}

void DynamicZone::DzMakeLeader(Client* client, std::string leader_name)
{
	if (!client || !ConfirmLeaderCommand(client))
	{
		return;
	}

	if (leader_name.empty())
	{
		client->MessageString(Chat::Red, DZ_LEADER_OFFLINE, leader_name.c_str());
		return;
	}

	// leader can only be changed by world
	SendWorldMakeLeaderRequest(client->CharacterID(), FormatName(leader_name));
}

void DynamicZone::DzRemovePlayer(Client* client, std::string name)
{
	if (!client || !ConfirmLeaderCommand(client))
	{
		return;
	}

	// live only seems to enforce min_players for requesting expeditions, no need to check here
	// note: on live members removed when inside a dz instance remain "temporary"
	// members for kick timer duration and still receive lockouts across zones (unimplemented)
	bool removed = RemoveMember(name);
	if (!removed)
	{
		client->MessageString(Chat::Red, DZ_NOT_MEMBER, FormatName(name).c_str());
	}
	else
	{
		client->MessageString(Chat::Yellow, DZ_REMOVED, FormatName(name).c_str(), m_name.c_str());
	}
}

void DynamicZone::DzQuit(Client* client)
{
	if (client)
	{
		RemoveMember(client->GetName());
	}
}

void DynamicZone::DzSwapPlayer(Client* client, std::string rem_name, std::string add_name)
{
	if (!client || !ConfirmLeaderCommand(client))
	{
		return;
	}

	if (rem_name.empty() || !HasMember(rem_name))
	{
		client->MessageString(Chat::Red, DZSWAP_CANNOT_REMOVE, FormatName(rem_name).c_str());
		return;
	}

	DzAddPlayer(client, add_name, rem_name);
}

void DynamicZone::DzPlayerList(Client* client)
{
	if (client)
	{
		client->MessageString(Chat::Yellow, DZ_LEADER, GetLeaderName().c_str());

		std::vector<std::string> names;
		for (const auto& member : m_members)
		{
			names.push_back(member.name);
		}

		client->MessageString(Chat::Yellow, DZ_MEMBERS, fmt::format("{}", fmt::join(names, ", ")).c_str());
	}
}

void DynamicZone::DzKickPlayers(Client* client)
{
	if (!client || !ConfirmLeaderCommand(client))
	{
		return;
	}

	RemoveAllMembers();
	client->MessageString(Chat::Red, DZ_REMOVED, "Everyone", m_name.c_str());
}

void DynamicZone::HandleWorldMessage(ServerPacket* pack)
{
	switch (pack->opcode)
	{
	case ServerOP_DzCreated:
	{
		auto buf = reinterpret_cast<ServerDzCreate_Struct*>(pack->pBuffer);
		if (zone && !zone->IsZone(buf->origin_zone_id, buf->origin_instance_id))
		{
			DynamicZone::CacheNewDynamicZone(pack);
		}
		break;
	}
	case ServerOP_DzDeleted:
	{
		// sent by world when it deletes an expired or empty dz
		// any system that held a reference to the dz should have already been notified
		auto buf = reinterpret_cast<ServerDzID_Struct*>(pack->pBuffer);
		auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
		if (zone && dz)
		{
			LogDynamicZonesDetail("Deleting dynamic zone [{}] from zone cache", buf->dz_id);
			dz->SendUpdatesToZoneMembers(true, true); // members silently removed
			zone->dynamic_zone_cache.erase(buf->dz_id);
		}
		break;
	}
	case ServerOP_DzAddPlayer:
	{
		auto buf = reinterpret_cast<ServerDzCommand_Struct*>(pack->pBuffer);
		if (buf->is_char_online)
		{
			if (auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id))
			{
				dz->DzAddPlayerContinue(buf->requester_name, buf->target_name, buf->remove_name);
			}
		}
		else if (Client* leader = entity_list.GetClientByName(buf->requester_name))
		{
			std::string target_name = FormatName(buf->target_name);
			leader->MessageString(Chat::Red, DZADD_NOT_ONLINE, target_name.c_str());
			leader->MessageString(Chat::Red, DZADD_INVITE_FAIL, target_name.c_str());
		}
		break;
	}
	case ServerOP_DzMakeLeader:
	{
		auto buf = reinterpret_cast<ServerDzCommandMakeLeader_Struct*>(pack->pBuffer);
		auto old_leader = entity_list.GetClientByCharID(buf->requester_id);

		// success flag is set by world to indicate new leader set to an online member
		if (old_leader && buf->is_success)
		{
			old_leader->MessageString(Chat::Yellow, DZ_LEADER_NAME, buf->new_leader_name);
		}
		else if (old_leader)
		{
			uint32_t str_id = buf->is_online ? DZ_NOT_MEMBER : DZ_LEADER_OFFLINE;
			old_leader->MessageString(Chat::Red, str_id, buf->new_leader_name);
		}

		if (buf->is_success && !RuleB(Expedition, AlwaysNotifyNewLeaderOnChange))
		{
			if (auto new_leader = entity_list.GetClientByName(buf->new_leader_name))
			{
				new_leader->MessageString(Chat::Yellow, DZ_LEADER_YOU);
			}
		}
		break;
	}
	case ServerOP_DzAddRemoveMember:
	{
		auto buf = reinterpret_cast<ServerDzMember_Struct*>(pack->pBuffer);
		if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
		{
			auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
			if (dz)
			{
				auto status = static_cast<DynamicZoneMemberStatus>(buf->character_status);
				dz->ProcessMemberAddRemove({ buf->character_id, buf->character_name, status }, buf->removed);
			}
		}

		if (zone && zone->IsZone(buf->dz_zone_id, buf->dz_instance_id))
		{
			// cache independent redundancy to kick removed members from dz's instance
			Client* client = entity_list.GetClientByCharID(buf->character_id);
			if (client)
			{
				client->SetDzRemovalTimer(buf->removed);
			}
		}
		break;
	}
	case ServerOP_DzSwapMembers:
	{
		auto buf = reinterpret_cast<ServerDzMemberSwap_Struct*>(pack->pBuffer);
		if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
		{
			auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
			if (dz)
			{
				auto status = static_cast<DynamicZoneMemberStatus>(buf->add_character_status);
				dz->ProcessMemberAddRemove({ buf->remove_character_id, buf->remove_character_name }, true);
				dz->ProcessMemberAddRemove({ buf->add_character_id, buf->add_character_name, status }, false);
			}
		}

		if (zone && zone->IsZone(buf->dz_zone_id, buf->dz_instance_id))
		{
			// cache independent redundancy to kick removed members from dz's instance
			Client* removed_client = entity_list.GetClientByCharID(buf->remove_character_id);
			if (removed_client)
			{
				removed_client->SetDzRemovalTimer(true);
			}

			Client* added_client = entity_list.GetClientByCharID(buf->add_character_id);
			if (added_client)
			{
				added_client->SetDzRemovalTimer(false);
			}
		}
		break;
	}
	case ServerOP_DzRemoveAllMembers:
	{
		auto buf = reinterpret_cast<ServerDzID_Struct*>(pack->pBuffer);
		if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
		{
			auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
			if (dz)
			{
				dz->ProcessRemoveAllMembers();
			}
		}

		if (zone && zone->IsZone(buf->dz_zone_id, buf->dz_instance_id))
		{
			// cache independent redundancy to kick removed members from dz's instance
			DynamicZone::StartAllClientRemovalTimers();
		}
		break;
	}
	case ServerOP_DzDurationUpdate:
	{
		auto buf = reinterpret_cast<ServerDzSetDuration_Struct*>(pack->pBuffer);
		auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
		if (dz)
		{
			dz->SetUpdatedDuration(buf->seconds);
		}
		break;
	}
	case ServerOP_DzSetCompass:
	case ServerOP_DzSetSafeReturn:
	case ServerOP_DzSetZoneIn:
	{
		auto buf = reinterpret_cast<ServerDzLocation_Struct*>(pack->pBuffer);
		if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
		{
			auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
			if (dz)
			{
				if (pack->opcode == ServerOP_DzSetCompass)
				{
					dz->SetCompass(buf->zone_id, buf->x, buf->y, buf->z, false);
				}
				else if (pack->opcode == ServerOP_DzSetSafeReturn)
				{
					dz->SetSafeReturn(buf->zone_id, buf->x, buf->y, buf->z, buf->heading, false);
				}
				else if (pack->opcode == ServerOP_DzSetZoneIn)
				{
					dz->SetZoneInLocation(buf->x, buf->y, buf->z, buf->heading, false);
				}
			}
		}
		break;
	}
	case ServerOP_DzSetSwitchID:
	{
		auto buf = reinterpret_cast<ServerDzSwitchID_Struct*>(pack->pBuffer);
		auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
		if (dz)
		{
			dz->ProcessSetSwitchID(buf->dz_switch_id);
		}
		break;
	}
	case ServerOP_DzGetMemberStatuses:
	{
		// reply from world for online member statuses request for async zone member updates
		auto buf = reinterpret_cast<ServerDzMemberStatuses_Struct*>(pack->pBuffer);
		auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
		if (dz)
		{
			for (uint32_t i = 0; i < buf->count; ++i)
			{
				auto status = static_cast<DynamicZoneMemberStatus>(buf->entries[i].online_status);
				dz->SetInternalMemberStatus(buf->entries[i].character_id, status);
			}
			dz->m_has_member_statuses = true;
			dz->SendUpdatesToZoneMembers(false, true);
		}
		break;
	}
	case ServerOP_DzGetBulkMemberStatuses:
	{
		if (zone)
		{
			std::vector<std::pair<uint32_t, std::vector<DynamicZoneMember>>> dzs;
			dzs.reserve(zone->dynamic_zone_cache.size());

			auto buf = reinterpret_cast<ServerDzCerealData_Struct*>(pack->pBuffer);
			EQ::Util::MemoryStreamReader ss(buf->cereal_data, buf->cereal_size);
			{
				cereal::BinaryInputArchive archive(ss);
				archive(dzs);
			}

			for (const auto& [dz_id, members] : dzs)
			{
				if (auto dz = DynamicZone::FindDynamicZoneByID(dz_id))
				{
					for (const auto& member : members)
					{
						dz->SetInternalMemberStatus(member.id, member.status);
					}
					dz->m_has_member_statuses = true;
				}
			}
		}
		break;
	}
	case ServerOP_DzUpdateMemberStatus:
	{
		auto buf = reinterpret_cast<ServerDzMemberStatus_Struct*>(pack->pBuffer);
		if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
		{
			auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
			if (dz)
			{
				auto status = static_cast<DynamicZoneMemberStatus>(buf->status);
				dz->ProcessMemberStatusChange(buf->character_id, status);
			}
		}
		break;
	}
	case ServerOP_DzLeaderChanged:
	{
		auto buf = reinterpret_cast<ServerDzLeaderID_Struct*>(pack->pBuffer);
		auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
		if (dz)
		{
			dz->ProcessLeaderChanged(buf->leader_id);
		}
		break;
	}
	case ServerOP_DzExpireWarning:
	{
		auto buf = reinterpret_cast<ServerDzExpireWarning_Struct*>(pack->pBuffer);
		auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
		if (dz)
		{
			dz->SendMembersExpireWarning(buf->minutes_remaining);
		}
		break;
	}
	case ServerOP_DzMovePC:
	{
		auto buf = reinterpret_cast<ServerDzMovePC_Struct*>(pack->pBuffer);
		auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
		if (dz)
		{
			Client* client = entity_list.GetClientByCharID(buf->character_id);
			if (client)
			{
				dz->MovePCInto(client, false);
			}
		}
		break;
	}
	case ServerOP_DzLock:
	{
		auto buf = reinterpret_cast<ServerDzLock_Struct*>(pack->pBuffer);
		if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
		{
			if (auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id))
			{
				dz->SetLocked(buf->lock, false, static_cast<DzLockMsg>(buf->lock_msg), buf->color);
			}
		}
		break;
	}
	case ServerOP_DzReplayOnJoin:
	{
		auto buf = reinterpret_cast<ServerDzBool_Struct*>(pack->pBuffer);
		if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
		{
			if (auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id))
			{
				dz->SetReplayOnJoin(buf->enabled);
			}
		}
		break;
	}
	case ServerOP_DzLockout:
	case ServerOP_DzLockoutDuration:
	{
		auto buf = reinterpret_cast<ServerDzLockout_Struct*>(pack->pBuffer);
		if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
		{
			if (DynamicZone* dz = DynamicZone::FindDynamicZoneByID(buf->dz_id))
			{
				DzLockout lockout{ dz->GetUUID(), dz->GetName(), buf->event_name, buf->expire_time, buf->duration };
				if (pack->opcode == ServerOP_DzLockout)
				{
					dz->HandleLockoutUpdate(lockout, buf->remove, buf->members_only);
				}
				else if (pack->opcode == ServerOP_DzLockoutDuration)
				{
					dz->HandleLockoutDuration(lockout, buf->seconds, buf->members_only, false);
				}
			}
		}
		break;
	}
	case ServerOP_DzCharacterLockout:
	{
		auto buf = reinterpret_cast<ServerDzCharacterLockout_Struct*>(pack->pBuffer);
		if (Client* client = entity_list.GetClientByCharID(buf->char_id))
		{
			if (!buf->remove)
			{
				client->AddDzLockout(DzLockout{ buf->uuid, buf->expedition, buf->event, buf->expire_time, buf->duration });
			}
			else if (buf->event[0] != '\0')
			{
				client->RemoveDzLockout(buf->expedition, buf->event);
			}
			else
			{
				client->RemoveDzLockouts(buf->expedition);
			}
		}
		break;
	}
	default:
		break;
	}
}

std::unique_ptr<EQApplicationPacket> DynamicZone::CreateExpireWarningPacket(uint32_t minutes_remaining)
{
	uint32_t outsize = sizeof(ExpeditionExpireWarning);
	auto outapp = std::make_unique<EQApplicationPacket>(OP_DzExpeditionEndsWarning, outsize);
	auto buf = reinterpret_cast<ExpeditionExpireWarning*>(outapp->pBuffer);
	buf->minutes_remaining = minutes_remaining;
	return outapp;
}

std::unique_ptr<EQApplicationPacket> DynamicZone::CreateInfoPacket(bool clear)
{
	constexpr uint32_t outsize = sizeof(DynamicZoneInfo_Struct);
	auto outapp = std::make_unique<EQApplicationPacket>(OP_DzExpeditionInfo, outsize);
	if (!clear)
	{
		auto info = reinterpret_cast<DynamicZoneInfo_Struct*>(outapp->pBuffer);
		info->assigned = true;
		strn0cpy(info->dz_name, m_name.c_str(), sizeof(info->dz_name));
		strn0cpy(info->leader_name, m_leader.name.c_str(), sizeof(info->leader_name));
		info->max_players = m_max_players;
	}
	return outapp;
}

std::unique_ptr<EQApplicationPacket> DynamicZone::CreateMemberListPacket(bool clear)
{
	uint32_t member_count = clear ? 0 : static_cast<uint32_t>(m_members.size());
	uint32_t member_entries_size = sizeof(DynamicZoneMemberEntry_Struct) * member_count;
	uint32_t outsize = sizeof(DynamicZoneMemberList_Struct) + member_entries_size;
	auto outapp = std::make_unique<EQApplicationPacket>(OP_DzMemberList, outsize);
	auto buf = reinterpret_cast<DynamicZoneMemberList_Struct*>(outapp->pBuffer);

	buf->member_count = member_count;

	if (!clear)
	{
		for (auto i = 0; i < m_members.size(); ++i)
		{
			strn0cpy(buf->members[i].name, m_members[i].name.c_str(), sizeof(buf->members[i].name));
			buf->members[i].online_status = static_cast<uint8_t>(m_members[i].status);
		}
	}

	return outapp;
}

std::unique_ptr<EQApplicationPacket> DynamicZone::CreateMemberNamePacket(const std::string& name, bool remove)
{
	constexpr uint32_t outsize = sizeof(DynamicZoneMemberListName_Struct);
	auto outapp = std::make_unique<EQApplicationPacket>(OP_DzMemberListName, outsize);
	auto buf = reinterpret_cast<DynamicZoneMemberListName_Struct*>(outapp->pBuffer);
	buf->add_name = !remove;
	strn0cpy(buf->name, name.c_str(), sizeof(buf->name));
	return outapp;
}

std::unique_ptr<EQApplicationPacket> DynamicZone::CreateMemberStatusPacket(const std::string& name, DynamicZoneMemberStatus status)
{
	// member list status uses member list struct with a single entry
	constexpr uint32_t outsize = sizeof(DynamicZoneMemberList_Struct) + sizeof(DynamicZoneMemberEntry_Struct);
	auto outapp = std::make_unique<EQApplicationPacket>(OP_DzMemberListStatus, outsize);
	auto buf = reinterpret_cast<DynamicZoneMemberList_Struct*>(outapp->pBuffer);
	buf->member_count = 1;

	auto entry = static_cast<DynamicZoneMemberEntry_Struct*>(buf->members);
	strn0cpy(entry->name, name.c_str(), sizeof(entry->name));
	entry->online_status = static_cast<uint8_t>(status);

	return outapp;
}

std::unique_ptr<EQApplicationPacket> DynamicZone::CreateLeaderNamePacket()
{
	constexpr uint32_t outsize = sizeof(DynamicZoneLeaderName_Struct);
	auto outapp = std::make_unique<EQApplicationPacket>(OP_DzSetLeaderName, outsize);
	auto buf = reinterpret_cast<DynamicZoneLeaderName_Struct*>(outapp->pBuffer);
	strn0cpy(buf->leader_name, m_leader.name.c_str(), sizeof(buf->leader_name));
	return outapp;
}

void DynamicZone::ProcessCompassChange(const DynamicZoneLocation& location)
{
	DynamicZoneBase::ProcessCompassChange(location);
	SendCompassUpdateToZoneMembers();
}

void DynamicZone::SendCompassUpdateToZoneMembers()
{
	for (const auto& member : m_members)
	{
		Client* member_client = entity_list.GetClientByCharID(member.id);
		if (member_client)
		{
			member_client->SendDzCompassUpdate();
		}
	}
}

void DynamicZone::ProcessSetSwitchID(int dz_switch_id)
{
	DynamicZoneBase::ProcessSetSwitchID(dz_switch_id);
	SendCompassUpdateToZoneMembers();
}

void DynamicZone::SendLeaderNameToZoneMembers()
{
	auto outapp_leader = CreateLeaderNamePacket();

	for (const auto& member : m_members)
	{
		Client* member_client = entity_list.GetClientByCharID(member.id);
		if (member_client)
		{
			member_client->QueuePacket(outapp_leader.get());

			if (member.id == m_leader.id && RuleB(Expedition, AlwaysNotifyNewLeaderOnChange))
			{
				member_client->MessageString(Chat::Yellow, DZ_LEADER_YOU);
			}
		}
	}
}

void DynamicZone::SendMembersExpireWarning(uint32_t minutes)
{
	// expeditions warn members in all zones not just the dz
	auto outapp = CreateExpireWarningPacket(minutes);
	for (const auto& member : GetMembers())
	{
		Client* client = entity_list.GetClientByCharID(member.id);
		if (client)
		{
			client->QueuePacket(outapp.get());

			// live doesn't actually send the chat message with it
			client->MessageString(Chat::Yellow, DZ_MINUTES_REMAIN, fmt::format_int(minutes).c_str());
		}
	}
}

void DynamicZone::SendMemberListToZoneMembers()
{
	auto outapp_members = CreateMemberListPacket(false);

	for (const auto& member : m_members)
	{
		Client* member_client = entity_list.GetClientByCharID(member.id);
		if (member_client)
		{
			member_client->QueuePacket(outapp_members.get());
		}
	}
}

void DynamicZone::SendMemberNameToZoneMembers(const std::string& char_name, bool remove)
{
	auto outapp = CreateMemberNamePacket(char_name, remove);

	for (const auto& member : m_members)
	{
		Client* member_client = entity_list.GetClientByCharID(member.id);
		if (member_client)
		{
			member_client->QueuePacket(outapp.get());
		}
	}
}

void DynamicZone::SendMemberStatusToZoneMembers(const DynamicZoneMember& update)
{
	auto outapp = CreateMemberStatusPacket(update.name, update.status);

	for (const auto& member : m_members)
	{
		Client* member_client = entity_list.GetClientByCharID(member.id);
		if (member_client)
		{
			member_client->QueuePacket(outapp.get());
		}
	}
}

void DynamicZone::SendClientWindowUpdate(Client* client)
{
	if (client)
	{
		client->QueuePacket(CreateInfoPacket().get());
		client->QueuePacket(CreateMemberListPacket().get());
	}
}

void DynamicZone::SendUpdatesToZoneMembers(bool removing_all, bool silent)
{
	// performs a full update on all members (usually for dz creation or removing all)
	if (!HasMembers())
	{
		return;
	}

	std::unique_ptr<EQApplicationPacket> outapp_info = nullptr;
	std::unique_ptr<EQApplicationPacket> outapp_members = nullptr;

	// only expeditions use the dz window. on live the window is filled by non
	// expeditions when first created but never kept updated. that behavior could
	// be replicated in the future by flagging this as a creation update
	if (m_type == DynamicZoneType::Expedition)
	{
		// clearing info also clears member list, no need to send both when removing
		outapp_info = CreateInfoPacket(removing_all);
		outapp_members = removing_all ? nullptr : CreateMemberListPacket();
	}

	for (const auto& member : GetMembers())
	{
		Client* client = entity_list.GetClientByCharID(member.id);
		if (client)
		{
			if (removing_all) {
				client->RemoveDynamicZoneID(GetID());
			} else {
				client->AddDynamicZoneID(GetID());
			}

			client->SendDzCompassUpdate();

			if (outapp_info)
			{
				client->QueuePacket(outapp_info.get());
			}

			if (outapp_members)
			{
				client->QueuePacket(outapp_members.get());
			}

			if (m_type == DynamicZoneType::Expedition && removing_all && !silent)
			{
				client->MessageString(Chat::Yellow, DZ_REMOVED, client->GetCleanName(), GetName().c_str());
			}
		}
	}
}

void DynamicZone::ProcessMemberAddRemove(const DynamicZoneMember& member, bool removed)
{
	DynamicZoneBase::ProcessMemberAddRemove(member, removed);

	// the affected client always gets a full compass update. for expeditions
	// client also gets window info update and all members get a member list update
	Client* client = entity_list.GetClientByCharID(member.id);
	if (client)
	{
		if (!removed) {
			client->AddDynamicZoneID(GetID());
		} else {
			client->RemoveDynamicZoneID(GetID());
		}

		client->SendDzCompassUpdate();

		if (m_type == DynamicZoneType::Expedition)
		{
			// sending clear info also clears member list for removed members
			client->QueuePacket(CreateInfoPacket(removed).get());
			client->MessageString(Chat::Yellow, removed ? DZ_REMOVED : DZ_ADDED, client->GetCleanName(), GetName().c_str());
		}
	}

	if (m_type == DynamicZoneType::Expedition)
	{
		// send full list when adding (MemberListName adds with "unknown" status)
		if (!removed) {
			SendMemberListToZoneMembers();
		} else {
			SendMemberNameToZoneMembers(member.name, true);
		}
	}
}

void DynamicZone::ProcessRemoveAllMembers()
{
	SendUpdatesToZoneMembers(true, false);
	DynamicZoneBase::ProcessRemoveAllMembers();
}

void DynamicZone::UpdateMembers()
{
	// gets member statuses from world and performs zone member updates on reply
	// if we've already received member statuses we can just update immediately
	if (m_has_member_statuses)
	{
		SendUpdatesToZoneMembers();
		return;
	}

	constexpr uint32_t pack_size = sizeof(ServerDzID_Struct);
	auto pack = std::make_unique<ServerPacket>(ServerOP_DzGetMemberStatuses, pack_size);
	auto buf = reinterpret_cast<ServerDzID_Struct*>(pack->pBuffer);
	buf->dz_id = GetID();
	buf->sender_zone_id = zone ? zone->GetZoneID() : 0;
	buf->sender_instance_id = zone ? zone->GetInstanceID() : 0;
	worldserver.SendPacket(pack.get());
}

bool DynamicZone::ProcessMemberStatusChange(uint32_t character_id, DynamicZoneMemberStatus status)
{
	bool changed = DynamicZoneBase::ProcessMemberStatusChange(character_id, status);

	if (changed && m_type == DynamicZoneType::Expedition)
	{
		auto member = GetMemberData(character_id);
		if (member.IsValid())
		{
			SendMemberStatusToZoneMembers(member);
		}
	}

	return changed;
}

void DynamicZone::ProcessLeaderChanged(uint32_t new_leader_id)
{
	auto new_leader = GetMemberData(new_leader_id);
	if (!new_leader.IsValid())
	{
		LogDynamicZones("Processed invalid new leader id [{}] for dz [{}]", new_leader_id, m_id);
		return;
	}

	LogDynamicZones("Replaced [{}] leader [{}] with [{}]", m_id, GetLeaderName(), new_leader.name);

	SetLeader(new_leader);
	if (GetType() == DynamicZoneType::Expedition)
	{
		SendLeaderNameToZoneMembers();
	}
}

bool DynamicZone::CanClientLootCorpse(Client* client, uint32_t npc_type_id, uint32_t entity_id)
{
	// non-members of a dz cannot loot corpses inside the dz
	if (!HasMember(client->CharacterID()))
	{
		return false;
	}

	// expeditions may prevent looting based on client's lockouts
	if (GetType() == DynamicZoneType::Expedition)
	{
		// entity id takes priority, falls back to checking by npc type if not set
		std::string event = GetLootEvent(entity_id, DzLootEvent::Type::Entity);
		if (event.empty())
		{
			event = GetLootEvent(npc_type_id, DzLootEvent::Type::NpcType);
		}

		if (!event.empty())
		{
			auto lockout = client->GetDzLockout(GetName(), event);
			if (!lockout || lockout->UUID() != m_uuid)
			{
				// client lockout not received in this expedition, prevent looting
				LogExpeditions("Character [{}] denied looting npc [{}] for [{}]", client->CharacterID(), npc_type_id, event);
				return false;
			}
		}
	}

	return true;
}

void DynamicZone::MovePCInto(Client* client, bool world_verify) const
{
	if (!world_verify)
	{
		DynamicZoneLocation zonein = GetZoneInLocation();
		ZoneMode zone_mode = HasZoneInLocation() ? ZoneMode::ZoneSolicited : ZoneMode::ZoneToSafeCoords;
		client->MovePC(GetZoneID(), GetInstanceID(), zonein.x, zonein.y, zonein.z, zonein.heading, 0, zone_mode);
	}
	else
	{
		ServerPacket pack(ServerOP_DzMovePC, sizeof(ServerDzMovePC_Struct));
		auto buf = reinterpret_cast<ServerDzMovePC_Struct*>(pack.pBuffer);
		buf->dz_id = GetID();
		buf->sender_zone_id = static_cast<uint16_t>(zone->GetZoneID());
		buf->sender_instance_id = static_cast<uint16_t>(zone->GetInstanceID());
		buf->character_id = client->CharacterID();
		worldserver.SendPacket(&pack);
	}
}

void DynamicZone::SetLocked(bool lock, bool update_db, DzLockMsg lock_msg, uint32_t color)
{
	DynamicZoneBase::SetLocked(lock, update_db, lock_msg, color);

	if (m_is_locked && lock_msg != DzLockMsg::None && IsCurrentZoneDz())
	{
		auto msg = lock_msg == DzLockMsg::Close ? LockClose : LockBegin;
		for (const auto& it : entity_list.GetClientList())
		{
			if (it.second)
			{
				it.second->Message(color, msg);
			}
		}
	}
}

void DynamicZone::SaveLockouts(const std::vector<DzLockout>& lockouts)
{
	m_lockouts = lockouts;
	DynamicZoneLockoutsRepository::InsertLockouts(database, m_id, m_lockouts);
}

static void SendWorldCharacterLockout(uint32_t char_id, const DzLockout& lockout, bool remove)
{
	uint32_t pack_size = sizeof(ServerDzCharacterLockout_Struct);
	ServerPacket pack(ServerOP_DzCharacterLockout, pack_size);
	auto buf = reinterpret_cast<ServerDzCharacterLockout_Struct*>(pack.pBuffer);
	buf->remove = remove;
	buf->char_id = char_id;
	buf->expire_time = lockout.GetExpireTime();
	buf->duration = lockout.GetDuration();
	strn0cpy(buf->uuid, lockout.UUID().c_str(), sizeof(buf->uuid));
	strn0cpy(buf->expedition, lockout.DzName().c_str(), sizeof(buf->expedition));
	strn0cpy(buf->event, lockout.Event().c_str(), sizeof(buf->event));
	worldserver.SendPacket(&pack);
}

void DynamicZone::AddCharacterLockout(
	uint32_t char_id, const std::string& expedition, const std::string& event, uint32_t seconds, const std::string& uuid)
{
	if (char_id)
	{
		auto lockout = DzLockout::Create(expedition, event, seconds, uuid);
		CharacterExpeditionLockoutsRepository::InsertLockouts(database, char_id, { lockout });
		SendWorldCharacterLockout(char_id, lockout, false);
	}
}

void DynamicZone::AddCharacterLockout(
	const std::string& char_name, const std::string& expedition, const std::string& event, uint32_t seconds, const std::string& uuid)
{
	if (!char_name.empty())
	{
		uint32_t char_id = database.GetCharacterID(char_name);
		AddCharacterLockout(char_id, expedition, event, seconds, uuid);
	}
}

bool DynamicZone::HasCharacterLockout(uint32_t char_id, const std::string& expedition, const std::string& event)
{
	auto lockouts = GetCharacterLockouts(char_id);
	return std::any_of(lockouts.begin(), lockouts.end(),
		[&](const auto& l) { return !l.IsExpired() && l.IsSame(expedition, event); });
}

bool DynamicZone::HasCharacterLockout(const std::string& char_name, const std::string& expedition, const std::string& event)
{
	if (!char_name.empty())
	{
		return HasCharacterLockout(database.GetCharacterID(char_name), expedition, event);
	}
	return false;
}

void DynamicZone::RemoveCharacterLockouts(uint32_t char_id, const std::string& expedition, const std::string& event)
{
	if (char_id)
	{
		std::string where = fmt::format("character_id = {}", char_id);
		if (!event.empty())
		{
			where += fmt::format(" AND expedition_name = '{}' AND event_name = '{}'", Strings::Escape(expedition), Strings::Escape(event));
		}
		else if (!expedition.empty())
		{
			where += fmt::format(" AND expedition_name = '{}'", Strings::Escape(expedition));
		}
		CharacterExpeditionLockoutsRepository::DeleteWhere(database, where);

		DzLockout lockout{{}, expedition, event, 0, 0};
		SendWorldCharacterLockout(char_id, lockout, true);
	}
}

void DynamicZone::RemoveCharacterLockouts(const std::string& char_name, const std::string& expedition, const std::string& event)
{
	if (!char_name.empty())
	{
		uint32_t char_id = database.GetCharacterID(char_name);
		RemoveCharacterLockouts(char_id, expedition, event);
	}
}

std::vector<DzLockout> DynamicZone::GetCharacterLockouts(uint32_t char_id)
{
	std::vector<DzLockout> lockouts;
	if (char_id == 0)
	{
		return lockouts;
	}

	auto client = entity_list.GetClientByCharID(char_id);
	if (client)
	{
		lockouts = client->GetDzLockouts();
	}
	else
	{
		lockouts = CharacterExpeditionLockoutsRepository::GetLockouts(database, char_id);
	}

	return lockouts;
}

void DynamicZone::AddClientsLockout(const DzLockout& lockout)
{
	std::vector<uint32_t> char_ids;
	for (const auto& it : entity_list.GetClientList())
	{
		char_ids.push_back(it.second->CharacterID());
		it.second->AddDzLockout(lockout);
	}

	if (!char_ids.empty())
	{
		CharacterExpeditionLockoutsRepository::InsertLockout(database, char_ids, lockout);
	}
}

void DynamicZone::HandleLockoutUpdate(const DzLockout& lockout, bool remove, bool members_only)
{
	DynamicZoneBase::HandleLockoutUpdate(lockout, remove, members_only);

	std::vector<uint32_t> char_ids;
	for (const auto& it : entity_list.GetClientList())
	{
		Client* client = it.second;
		if (std::ranges::any_of(m_members, [&](const auto& m) { return m.id == client->CharacterID(); }))
		{
			if (!remove)
			{
				client->AddDzLockout(lockout);
			}
			else
			{
				client->RemoveDzLockout(GetName(), lockout.Event());
			}
		}
		else if (!remove && IsCurrentZoneDz()) // non-member client inside the dz
		{
			// all clients inside the dz instance receive added lockouts to avoid exploits
			// where members quit the expedition but haven't been kicked from zone yet
			char_ids.push_back(client->CharacterID());
			client->AddDzLockout(lockout);
		}
	}

	if (!char_ids.empty())
	{
		CharacterExpeditionLockoutsRepository::InsertLockout(database, char_ids, lockout);
	}
}

void DynamicZone::HandleLockoutDuration(const DzLockout& lockout, int seconds, bool members_only, bool insert_db)
{
	DynamicZoneBase::HandleLockoutDuration(lockout, seconds, members_only, insert_db);

	std::vector<uint32_t> char_ids;
	for (const auto& it : entity_list.GetClientList())
	{
		Client* client = it.second;
		if (std::ranges::any_of(m_members, [&](const auto& m) { return m.id == client->CharacterID(); }))
		{
			client->AddDzLockoutDuration(lockout, seconds, m_uuid);
		}
		else if (IsCurrentZoneDz()) // non-member client inside the dz
		{
			char_ids.push_back(client->CharacterID());
			client->AddDzLockoutDuration(lockout, seconds, m_uuid);
		}
	}

	if (!char_ids.empty()) // always update db for non-members in dz (call may be from another zone)
	{
		int secs = static_cast<int>(seconds * RuleR(Expedition, LockoutDurationMultiplier));
		CharacterExpeditionLockoutsRepository::AddLockoutDuration(database, char_ids, lockout, secs);
	}
}

void DynamicZone::SetLootEvent(uint32_t id, const std::string& event, DzLootEvent::Type type)
{
	if (id != 0 && IsCurrentZoneDz())
	{
		LogExpeditions("Setting loot event [{}] for id [{}] type [{}]", event, id, static_cast<int>(type));
		auto it = std::ranges::find_if(m_loot_events, [&](const auto& le) { return le.id == id && le.type == type; });
		if (it != m_loot_events.end())
		{
			it->event = event;
		}
		else
		{
			m_loot_events.push_back({id, event, type});
		}
	}
}

std::string DynamicZone::GetLootEvent(uint32_t id, DzLootEvent::Type type) const
{
	std::string event_name;
	if (id != 0 && IsCurrentZoneDz())
	{
		auto it = std::ranges::find_if(m_loot_events, [&](const auto& le) { return le.id == id && le.type == type; });
		if (it != m_loot_events.end())
		{
			event_name = it->event;
		}
	}
	return event_name;
}

std::unique_ptr<EQApplicationPacket> DynamicZone::CreateInvitePacket(const std::string& inviter, const std::string& swap_name)
{
	uint32_t outsize = sizeof(ExpeditionInvite_Struct);
	auto outapp = std::make_unique<EQApplicationPacket>(OP_DzExpeditionInvite, outsize);
	auto outbuf = reinterpret_cast<ExpeditionInvite_Struct*>(outapp->pBuffer);
	strn0cpy(outbuf->inviter_name, inviter.c_str(), sizeof(outbuf->inviter_name));
	strn0cpy(outbuf->expedition_name, GetName().c_str(), sizeof(outbuf->expedition_name));
	strn0cpy(outbuf->swap_name, swap_name.c_str(), sizeof(outbuf->swap_name));
	outbuf->swapping = !swap_name.empty();
	outbuf->dz_zone_id = GetZoneID();
	outbuf->dz_instance_id = GetInstanceID();
	return outapp;
}

void DynamicZone::SendWorldPendingInvite(const ExpeditionInvite& invite, const std::string& add_name)
{
	LogExpeditions("[{}] saving invite from [{}] to [{}]", add_name, invite.inviter_name, invite.dz_id);
	SendWorldPlayerInvite(invite.inviter_name, invite.swap_name, add_name, true);
}

void DynamicZone::SendWorldPlayerInvite(const std::string& inviter, const std::string& swap_name, const std::string& add_name, bool pending)
{
	auto opcode = pending ? ServerOP_DzSaveInvite : ServerOP_DzAddPlayer;
	ServerPacket pack(opcode, static_cast<uint32_t>(sizeof(ServerDzCommand_Struct)));
	auto buf = reinterpret_cast<ServerDzCommand_Struct*>(pack.pBuffer);
	buf->dz_id = GetID();
	buf->is_char_online = false;
	strn0cpy(buf->requester_name, inviter.c_str(), sizeof(buf->requester_name));
	strn0cpy(buf->target_name, add_name.c_str(), sizeof(buf->target_name));
	strn0cpy(buf->remove_name, swap_name.c_str(), sizeof(buf->remove_name));
	worldserver.SendPacket(&pack);
}

void DynamicZone::SendWorldMakeLeaderRequest(uint32_t char_id, const std::string& leader_name)
{
	ServerPacket pack(ServerOP_DzMakeLeader, static_cast<uint32_t>(sizeof(ServerDzCommandMakeLeader_Struct)));
	auto buf = reinterpret_cast<ServerDzCommandMakeLeader_Struct*>(pack.pBuffer);
	buf->dz_id = GetID();
	buf->requester_id = char_id;
	strn0cpy(buf->new_leader_name, leader_name.c_str(), sizeof(buf->new_leader_name));
	worldserver.SendPacket(&pack);
}
