ArenaController.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// Networked identity for the complete arena configuration.
///
/// These are stable ArenaLayout.LayoutId values, not scene-child indices.
/// The host chooses one state; each peer locally instantiates the matching
/// prefab assets under its three arena section anchors.
/// </summary>
public struct ArenaLayoutState : IEquatable<ArenaLayoutState>
{
	public const int InvalidId = -1;

	public int SpawnAId { get; set; }
	public int CenterId { get; set; }
	public int SpawnBId { get; set; }

	public static ArenaLayoutState Invalid =>
		new()
		{
			SpawnAId = InvalidId,
			CenterId = InvalidId,
			SpawnBId = InvalidId
		};

	public bool IsValid =>
		SpawnAId >= 0 &&
		CenterId >= 0 &&
		SpawnBId >= 0;

	public bool Equals( ArenaLayoutState other )
	{
		return
			SpawnAId == other.SpawnAId &&
			CenterId == other.CenterId &&
			SpawnBId == other.SpawnBId;
	}

	public override bool Equals( object obj )
	{
		return
			obj is ArenaLayoutState other &&
			Equals( other );
	}

	public override int GetHashCode()
	{
		return HashCode.Combine(
			SpawnAId,
			CenterId,
			SpawnBId
		);
	}

	public static bool operator ==(
		ArenaLayoutState left,
		ArenaLayoutState right
	)
	{
		return left.Equals( right );
	}

	public static bool operator !=(
		ArenaLayoutState left,
		ArenaLayoutState right
	)
	{
		return !left.Equals( right );
	}

	public override string ToString()
	{
		return $"A:{SpawnAId} C:{CenterId} B:{SpawnBId}";
	}
}

/// <summary>
/// Arena System v2.1 — runtime prefab layouts.
///
/// main.scene contains only the three section anchors plus this controller.
/// Layout assets live as prefabs in the controller catalog. When the networked
/// ArenaLayoutState changes, each peer locally replaces the three active
/// runtime instances.
///
/// Competitive rounds use the SAME Spawn prefab ID for A and B; Spawn B is
/// mirrored by the 180-degree Yaw on SpawnBSection. Sandbox may still choose
/// Spawn A and Spawn B independently.
///
/// Runtime clones are intentionally local/non-networked. Only the compact
/// ArenaLayoutState is synchronized.
/// </summary>
[Title( "Arena Controller" )]
[Category( "One More Round" )]
[Icon( "grid_view" )]
public sealed class ArenaController : Component
{
	// =========================================================
	// SECTION ANCHORS
	// =========================================================

	[Property, Group( "Sections" )]
	public GameObject SpawnASection { get; set; }

	[Property, Group( "Sections" )]
	public GameObject CenterSection { get; set; }

	[Property, Group( "Sections" )]
	public GameObject SpawnBSection { get; set; }

	// =========================================================
	// PREFAB CATALOG
	// =========================================================

	/// <summary>
	/// Spawn layout prefab assets. The SAME catalog is used for Spawn A and
	/// Spawn B. Drag prefab assets here directly from the Asset Browser.
	/// </summary>
	[Property, Group( "Catalog" )]
	public List<GameObject> SpawnLayoutPrefabs { get; set; } = new();

	/// <summary>
	/// Center layout prefab assets.
	/// </summary>
	[Property, Group( "Catalog" )]
	public List<GameObject> CenterLayoutPrefabs { get; set; } = new();

	// =========================================================
	// NETWORKED STATE
	// =========================================================

	[Sync( SyncFlags.FromHost )]
	[Change( nameof( OnCurrentLayoutStateChanged ) )]
	public ArenaLayoutState CurrentLayoutState { get; set; } =
		ArenaLayoutState.Invalid;

	// =========================================================
	// DIAGNOSTICS
	// =========================================================

	[Property, Group( "Diagnostics" )]
	public bool LogLayoutChanges { get; set; } = true;

	[Property, Group( "Diagnostics" )]
	public bool WarnAboutNonNormalizedLayoutRoots { get; set; } = true;

	// =========================================================
	// PUBLIC INFO FOR SANDBOX SETUP UI
	// =========================================================

	public int SpawnALayoutCount
	{
		get
		{
			EnsureCatalogs();
			return _spawnLayouts.Count;
		}
	}

	public int CenterLayoutCount
	{
		get
		{
			EnsureCatalogs();
			return _centerLayouts.Count;
		}
	}

	public int SpawnBLayoutCount
	{
		get
		{
			EnsureCatalogs();
			return _spawnLayouts.Count;
		}
	}

	public string GetSpawnALayoutName( int index )
	{
		EnsureCatalogs();
		return GetLayoutNameByIndex(
			_spawnLayouts,
			index
		);
	}

	public string GetCenterLayoutName( int index )
	{
		EnsureCatalogs();
		return GetLayoutNameByIndex(
			_centerLayouts,
			index
		);
	}

	public string GetSpawnBLayoutName( int index )
	{
		EnsureCatalogs();
		return GetLayoutNameByIndex(
			_spawnLayouts,
			index
		);
	}

	// =========================================================
	// CATALOG DATA
	// =========================================================

	private sealed class LayoutEntry
	{
		public int Id { get; init; }
		public string Name { get; init; }
		public GameObject Prefab { get; init; }
		public ArenaLayout Metadata { get; init; }
	}

	private struct ArenaCombination
	{
		public int SpawnId { get; set; }
		public int CenterId { get; set; }

		public ArenaLayoutState ToSymmetricState()
		{
			return new ArenaLayoutState
			{
				SpawnAId = SpawnId,
				CenterId = CenterId,
				SpawnBId = SpawnId
			};
		}
	}

	private readonly List<LayoutEntry> _spawnLayouts =
		new();

	private readonly List<LayoutEntry> _centerLayouts =
		new();

	private readonly Dictionary<int, LayoutEntry> _spawnById =
		new();

	private readonly Dictionary<int, LayoutEntry> _centerById =
		new();

	private bool _catalogsBuilt;

	// =========================================================
	// LIVE RUNTIME INSTANCES
	// =========================================================

	private GameObject _spawnAInstance;
	private GameObject _centerInstance;
	private GameObject _spawnBInstance;

	private int _spawnAInstanceId =
		ArenaLayoutState.InvalidId;

	private int _centerInstanceId =
		ArenaLayoutState.InvalidId;

	private int _spawnBInstanceId =
		ArenaLayoutState.InvalidId;

	// =========================================================
	// MATCH SELECTION STATE
	// =========================================================

	private readonly Dictionary<RoundModifier, List<ArenaCombination>> _bags =
		new();

	private readonly Dictionary<RoundModifier, ArenaLayoutState> _lastStateByModifier =
		new();

	private int _preparedRoundNumber =
		-1;

	private RoundModifier _preparedModifier =
		RoundModifier.Standard;

	private ArenaLayoutUsage _preparedUsage =
		ArenaLayoutUsage.Standard;

	private ArenaLayoutState _preparedRoundState =
		ArenaLayoutState.Invalid;

	// =========================================================
	// LIFECYCLE
	// =========================================================

	protected override void OnStart()
	{
		RebuildCatalogs();

		// If a synchronized state already exists (late join / scene startup),
		// materialize it immediately. Otherwise the section anchors remain empty
		// until lobby/round preparation chooses the first state.
		if ( CurrentLayoutState.IsValid )
		{
			ApplyLayoutState(
				CurrentLayoutState
			);
		}
	}

	private void OnCurrentLayoutStateChanged(
		ArenaLayoutState oldState,
		ArenaLayoutState newState
	)
	{
		EnsureCatalogs();

		ApplyLayoutState(
			newState
		);
	}

	// =========================================================
	// ROUND / LOBBY PREPARATION
	// =========================================================

	/// <summary>
	/// Clears only host-side shuffle/history state for a new match.
	/// It does not change the visible arena by itself.
	/// </summary>
	public void ResetMatchSequence()
	{
		if ( !Networking.IsHost )
			return;

		_bags.Clear();
		_lastStateByModifier.Clear();

		_preparedRoundNumber =
			-1;

		_preparedModifier =
			RoundModifier.Standard;

		_preparedUsage =
			ArenaLayoutUsage.Standard;

		_preparedRoundState =
			ArenaLayoutState.Invalid;
	}

	/// <summary>
	/// Uses a deterministic, non-consuming Lobby configuration.
	/// </summary>
	public void PrepareLobby()
	{
		if ( !Networking.IsHost )
			return;

		EnsureCatalogs();

		ArenaLayoutState state =
			FindFirstSymmetricState(
				ArenaLayoutUsage.Lobby
			);

		if ( !state.IsValid )
		{
			Log.Warning(
				"ARENA | No Lobby layout combination found. Falling back to Standard."
			);

			state =
				FindFirstSymmetricState(
					ArenaLayoutUsage.Standard
				);
		}

		if ( !state.IsValid )
		{
			Log.Error(
				"ARENA | Lobby could not be prepared because no valid layout combination exists."
			);

			return;
		}

		SetCurrentState(
			state
		);

		if ( LogLayoutChanges )
		{
			Log.Info(
				$"ARENA | LOBBY | {DescribeState( state )}"
			);
		}
	}

	/// <summary>
	/// Prepares the arena for a competitive round.
	///
	/// Repeated calls for the same round/modifier are idempotent and reuse the
	/// already-selected state instead of consuming another shuffle-bag entry.
	/// </summary>
	public void PrepareForRound(
		int roundNumber,
		RoundModifier modifier
	)
	{
		PrepareForRound(
			roundNumber,
			modifier,
			GetUsageForModifier( modifier )
		);
	}

	/// <summary>
	/// Data-driven overload used by OMRRoundRuleProfile. The modifier remains the
	/// shuffle/history key while the profile chooses which authored layout usage
	/// bucket this round should consume.
	/// </summary>
	public void PrepareForRound(
		int roundNumber,
		RoundModifier modifier,
		ArenaLayoutUsage usage
	)
	{
		if ( !Networking.IsHost )
			return;

		EnsureCatalogs();

		if ( !ValidateRuntimeRequirements() )
			return;

		if (
			_preparedRoundNumber == roundNumber &&
			_preparedModifier == modifier &&
			_preparedUsage == usage &&
			_preparedRoundState.IsValid
		)
		{
			SetCurrentState(
				_preparedRoundState
			);

			return;
		}

		ArenaLayoutState state =
			TakeNextStateFromBag(
				modifier,
				usage
			);

		if ( !state.IsValid )
		{
			Log.Warning(
				$"ARENA | No valid {modifier} combination found. Falling back to Standard layouts."
			);

			state =
				TakeNextStateFromBag(
					RoundModifier.Standard,
					ArenaLayoutUsage.Standard
				);
		}

		if ( !state.IsValid )
		{
			Log.Error(
				$"ARENA | ROUND {roundNumber} could not prepare a valid arena."
			);

			return;
		}

		_preparedRoundNumber =
			roundNumber;

		_preparedModifier =
			modifier;

		_preparedUsage =
			usage;

		_preparedRoundState =
			state;

		SetCurrentState(
			state
		);

		if ( LogLayoutChanges )
		{
			Log.Info(
				$"ARENA | ROUND {roundNumber} | MODIFIER {modifier} | {DescribeState( state )}"
			);
		}
	}

	/// <summary>
	/// Sandbox keeps independent Spawn A / Center / Spawn B selection by the
	/// existing UI indices. Both Spawn sides use the same prefab catalog.
	/// </summary>
	public void PrepareSandbox(
		int spawnAIndex,
		int centerIndex,
		int spawnBIndex
	)
	{
		if ( !Networking.IsHost )
			return;

		EnsureCatalogs();

		LayoutEntry spawnA =
			GetLayoutByIndex(
				_spawnLayouts,
				spawnAIndex
			);

		LayoutEntry center =
			GetLayoutByIndex(
				_centerLayouts,
				centerIndex
			);

		LayoutEntry spawnB =
			GetLayoutByIndex(
				_spawnLayouts,
				spawnBIndex
			);

		if (
			spawnA is null ||
			center is null ||
			spawnB is null
		)
		{
			Log.Warning(
				"ARENA | SANDBOX | One or more requested layout catalogs are empty."
			);

			return;
		}

		ArenaLayoutState state =
			new()
			{
				SpawnAId = spawnA.Id,
				CenterId = center.Id,
				SpawnBId = spawnB.Id
			};

		SetCurrentState(
			state
		);

		if ( LogLayoutChanges )
		{
			Log.Info(
				$"ARENA | SANDBOX | {DescribeState( state )}"
			);
		}
	}

	// =========================================================
	// APPLY NETWORKED STATE
	// =========================================================

	private void SetCurrentState(
		ArenaLayoutState state
	)
	{
		CurrentLayoutState =
			state;

		// Explicit apply is useful when reapplying an identical state because a
		// [Change] callback only needs to react to an actual property change.
		ApplyLayoutState(
			state
		);
	}

	private void ApplyLayoutState(
		ArenaLayoutState state
	)
	{
		if ( !state.IsValid )
		{
			ClearRuntimeLayouts();
			return;
		}

		ReplaceSectionInstance(
			SpawnASection,
			_spawnById,
			state.SpawnAId,
			ref _spawnAInstance,
			ref _spawnAInstanceId,
			"Spawn A"
		);

		ReplaceSectionInstance(
			CenterSection,
			_centerById,
			state.CenterId,
			ref _centerInstance,
			ref _centerInstanceId,
			"Center"
		);

		ReplaceSectionInstance(
			SpawnBSection,
			_spawnById,
			state.SpawnBId,
			ref _spawnBInstance,
			ref _spawnBInstanceId,
			"Spawn B"
		);
	}

	private void ReplaceSectionInstance(
		GameObject section,
		Dictionary<int, LayoutEntry> catalog,
		int requestedId,
		ref GameObject liveInstance,
		ref int liveId,
		string sectionName
	)
	{
		if (
			liveId == requestedId &&
			liveInstance is not null &&
			liveInstance.IsValid
		)
		{
			return;
		}

		RemoveRuntimeInstance(
			ref liveInstance,
			ref liveId
		);

		if (
			section is null ||
			!section.IsValid
		)
		{
			Log.Error(
				$"ARENA | {sectionName} section anchor is missing."
			);

			return;
		}

		if (
			!catalog.TryGetValue(
				requestedId,
				out LayoutEntry entry
			) ||
			entry.Prefab is null ||
			!entry.Prefab.IsValid
		)
		{
			Log.Warning(
				$"ARENA | {sectionName} has no prefab with LayoutId {requestedId}."
			);

			return;
		}

		GameObject instance =
			entry.Prefab.Clone();

		if (
			instance is null ||
			!instance.IsValid
		)
		{
			Log.Error(
				$"ARENA | Failed to clone {sectionName} layout {requestedId}:{entry.Name}."
			);

			return;
		}

		// Runtime arena geometry is deterministic local content. We synchronize
		// only ArenaLayoutState; we do not network-spawn three extra hierarchies.
		instance.Flags |=
			GameObjectFlags.NotNetworked;

		instance.Parent =
			section;

		// Normalize the cloned prefab root after parenting it under the
		// section anchor. Use GameObject local transform properties directly
		// here; inside a Component class, `Transform` also names the inherited
		// GameTransform property, which makes `Transform.Zero` resolve to the
		// wrong symbol in current S&box.
		instance.LocalPosition =
			Vector3.Zero;

		instance.LocalRotation =
			Rotation.Identity;

		instance.LocalScale =
			Vector3.One;

		instance.Enabled =
			true;

		liveInstance =
			instance;

		liveId =
			requestedId;

		if ( LogLayoutChanges )
		{
			Log.Info(
				$"ARENA | SPAWNED {sectionName} | {requestedId}:{entry.Name}"
			);
		}
	}

	private void RemoveRuntimeInstance(
		ref GameObject instance,
		ref int id
	)
	{
		if (
			instance is not null &&
			instance.IsValid
		)
		{
			// Disable immediately so old colliders/renderers cannot overlap the
			// incoming layout during the frame in which Destroy is processed.
			instance.Enabled =
				false;

			instance.Destroy();
		}

		instance =
			null;

		id =
			ArenaLayoutState.InvalidId;
	}

	private void ClearRuntimeLayouts()
	{
		RemoveRuntimeInstance(
			ref _spawnAInstance,
			ref _spawnAInstanceId
		);

		RemoveRuntimeInstance(
			ref _centerInstance,
			ref _centerInstanceId
		);

		RemoveRuntimeInstance(
			ref _spawnBInstance,
			ref _spawnBInstanceId
		);
	}

	// =========================================================
	// SHUFFLE BAGS
	// =========================================================

	private ArenaLayoutState TakeNextStateFromBag(
		RoundModifier modifier,
		ArenaLayoutUsage usage
	)
	{
		if (
			!_bags.TryGetValue(
				modifier,
				out List<ArenaCombination> bag
			) ||
			bag.Count == 0
		)
		{
			bag =
				BuildSymmetricCombinations(
					usage
				);

			if ( bag.Count == 0 )
				return ArenaLayoutState.Invalid;

			Shuffle(
				bag
			);

			AvoidImmediateRepeatAcrossBagBoundary(
				modifier,
				bag
			);

			_bags[modifier] =
				bag;
		}

		int lastIndex =
			bag.Count - 1;

		ArenaCombination combination =
			bag[lastIndex];

		bag.RemoveAt(
			lastIndex
		);

		ArenaLayoutState state =
			combination.ToSymmetricState();

		_lastStateByModifier[modifier] =
			state;

		return state;
	}

	private List<ArenaCombination> BuildSymmetricCombinations(
		ArenaLayoutUsage usage
	)
	{
		List<int> spawnIds =
			_spawnLayouts
				.Where(
					x =>
						x.Metadata.Supports(
							usage
						)
				)
				.Select(
					x => x.Id
				)
				.Distinct()
				.OrderBy(
					x => x
				)
				.ToList();

		List<int> centerIds =
			_centerLayouts
				.Where(
					x =>
						x.Metadata.Supports(
							usage
						)
				)
				.Select(
					x => x.Id
				)
				.Distinct()
				.OrderBy(
					x => x
				)
				.ToList();

		List<ArenaCombination> combinations =
			new();

		foreach (
			int spawnId in spawnIds
		)
		{
			foreach (
				int centerId in centerIds
			)
			{
				combinations.Add(
					new ArenaCombination
					{
						SpawnId = spawnId,
						CenterId = centerId
					}
				);
			}
		}

		return combinations;
	}

	private void Shuffle(
		List<ArenaCombination> bag
	)
	{
		for (
			int i = bag.Count - 1;
			i > 0;
			i--
		)
		{
			int swapIndex =
				Game.Random.Next(
					0,
					i + 1
				);

			(
				bag[i],
				bag[swapIndex]
			) = (
				bag[swapIndex],
				bag[i]
			);
		}
	}

	private void AvoidImmediateRepeatAcrossBagBoundary(
		RoundModifier modifier,
		List<ArenaCombination> bag
	)
	{
		if (
			bag.Count <= 1 ||
			!_lastStateByModifier.TryGetValue(
				modifier,
				out ArenaLayoutState previous
			)
		)
			return;

		int nextIndex =
			bag.Count - 1;

		ArenaLayoutState next =
			bag[nextIndex]
				.ToSymmetricState();

		if ( next != previous )
			return;

		(
			bag[nextIndex],
			bag[0]
		) = (
			bag[0],
			bag[nextIndex]
		);
	}

	// =========================================================
	// PREFAB CATALOG
	// =========================================================

	private void EnsureCatalogs()
	{
		if ( _catalogsBuilt )
			return;

		RebuildCatalogs();
	}

	private void RebuildCatalogs()
	{
		BuildPrefabCatalog(
			SpawnLayoutPrefabs,
			ArenaLayoutSection.Spawn,
			_spawnLayouts,
			_spawnById
		);

		BuildPrefabCatalog(
			CenterLayoutPrefabs,
			ArenaLayoutSection.Center,
			_centerLayouts,
			_centerById
		);

		_catalogsBuilt =
			true;

		_bags.Clear();
	}

	private void BuildPrefabCatalog(
		IEnumerable<GameObject> prefabReferences,
		ArenaLayoutSection expectedSection,
		List<LayoutEntry> entries,
		Dictionary<int, LayoutEntry> byId
	)
	{
		entries.Clear();
		byId.Clear();

		if ( prefabReferences is null )
			return;

		foreach (
			GameObject prefab in prefabReferences
		)
		{
			if (
				prefab is null ||
				!prefab.IsValid
			)
				continue;

			ArenaLayout metadata =
				prefab.GetComponent<ArenaLayout>(
					true
				);

			if ( metadata is null )
				continue;

			LayoutEntry entry =
				new()
				{
					Id = metadata.LayoutId,
					Name = metadata.GetDisplayName(),
					Prefab = prefab,
					Metadata = metadata
				};

			entries.Add(
				entry
			);

			if ( !byId.ContainsKey( entry.Id ) )
			{
				byId.Add(
					entry.Id,
					entry
				);
			}
		}

		entries.Sort(
			(a, b) =>
				a.Id.CompareTo(
					b.Id
				)
		);
	}

	// =========================================================
	// LOOKUP / UI HELPERS
	// =========================================================

	private LayoutEntry GetLayoutByIndex(
		List<LayoutEntry> entries,
		int index
	)
	{
		if (
			entries is null ||
			entries.Count == 0
		)
			return null;

		index =
			Math.Clamp(
				index,
				0,
				entries.Count - 1
			);

		return entries[index];
	}

	private string GetLayoutNameByIndex(
		List<LayoutEntry> entries,
		int index
	)
	{
		LayoutEntry entry =
			GetLayoutByIndex(
				entries,
				index
			);

		if ( entry is null )
			return "NONE";

		return entry.Name
			.ToUpperInvariant();
	}

	private ArenaLayoutState FindFirstSymmetricState(
		ArenaLayoutUsage usage
	)
	{
		List<ArenaCombination> combinations =
			BuildSymmetricCombinations(
				usage
			);

		if ( combinations.Count == 0 )
			return ArenaLayoutState.Invalid;

		ArenaCombination first =
			combinations
				.OrderBy(
					x => x.SpawnId
				)
				.ThenBy(
					x => x.CenterId
				)
				.First();

		return first.ToSymmetricState();
	}

	private ArenaLayoutUsage GetUsageForModifier(
		RoundModifier modifier
	)
	{
		return modifier switch
		{
			RoundModifier.HighNoon =>
				ArenaLayoutUsage.HighNoon,

			_ =>
				ArenaLayoutUsage.Standard
		};
	}

	private string DescribeState(
		ArenaLayoutState state
	)
	{
		string spawnAName =
			GetNameById(
				_spawnById,
				state.SpawnAId
			);

		string centerName =
			GetNameById(
				_centerById,
				state.CenterId
			);

		string spawnBName =
			GetNameById(
				_spawnById,
				state.SpawnBId
			);

		return
			$"SPAWN A {state.SpawnAId}:{spawnAName} | " +
			$"CENTER {state.CenterId}:{centerName} | " +
			$"SPAWN B {state.SpawnBId}:{spawnBName}";
	}

	private string GetNameById(
		Dictionary<int, LayoutEntry> byId,
		int id
	)
	{
		if (
			byId.TryGetValue(
				id,
				out LayoutEntry entry
			)
		)
		{
			return entry.Name;
		}

		return "MISSING";
	}

	// =========================================================
	// VALIDATION
	// =========================================================

	[Button, Title( "Validate Arena" ), Description( "Rebuild the prefab catalog and report arena setup problems." )]
	public void ValidateArenaSetup()
	{
		RebuildCatalogs();

		int errors =
			0;

		int warnings =
			0;

		ValidateSectionAnchor(
			"Spawn A",
			SpawnASection,
			ref errors,
			ref warnings
		);

		ValidateSectionAnchor(
			"Center",
			CenterSection,
			ref errors,
			ref warnings
		);

		ValidateSectionAnchor(
			"Spawn B",
			SpawnBSection,
			ref errors,
			ref warnings
		);

		ValidatePrefabCatalog(
			"Spawn",
			SpawnLayoutPrefabs,
			ArenaLayoutSection.Spawn,
			_spawnLayouts,
			ref errors,
			ref warnings
		);

		ValidatePrefabCatalog(
			"Center",
			CenterLayoutPrefabs,
			ArenaLayoutSection.Center,
			_centerLayouts,
			ref errors,
			ref warnings
		);

		int standardCount =
			BuildSymmetricCombinations(
				ArenaLayoutUsage.Standard
			).Count;

		int lobbyCount =
			BuildSymmetricCombinations(
				ArenaLayoutUsage.Lobby
			).Count;

		int highNoonCount =
			BuildSymmetricCombinations(
				ArenaLayoutUsage.HighNoon
			).Count;

		if ( standardCount == 0 )
		{
			errors++;

			Log.Error(
				"ARENA VALIDATION | No Standard combinations exist."
			);
		}

		if ( highNoonCount == 0 )
		{
			errors++;

			Log.Error(
				"ARENA VALIDATION | No High Noon combination exists."
			);
		}

		if ( lobbyCount == 0 )
		{
			warnings++;

			Log.Warning(
				"ARENA VALIDATION | No Lobby combination is marked. PrepareLobby will fall back to Standard."
			);
		}

		if ( errors == 0 )
		{
			Log.Info(
				$"ARENA VALIDATION | PASS | Spawn:{_spawnLayouts.Count} | Center:{_centerLayouts.Count} | Standard:{standardCount} | Lobby:{lobbyCount} | HighNoon:{highNoonCount} | Warnings:{warnings}"
			);
		}
		else
		{
			Log.Error(
				$"ARENA VALIDATION | FAIL | Errors:{errors} | Warnings:{warnings}"
			);
		}
	}

	private bool ValidateRuntimeRequirements()
	{
		if (
			SpawnASection is null ||
			CenterSection is null ||
			SpawnBSection is null
		)
		{
			Log.Error(
				"ARENA | Section anchor references are missing."
			);

			return false;
		}

		if (
			_spawnLayouts.Count == 0 ||
			_centerLayouts.Count == 0
		)
		{
			Log.Error(
				"ARENA | Spawn or Center prefab catalog is empty."
			);

			return false;
		}

		return true;
	}

	private void ValidateSectionAnchor(
		string sectionName,
		GameObject section,
		ref int errors,
		ref int warnings
	)
	{
		if (
			section is null ||
			!section.IsValid
		)
		{
			errors++;

			Log.Error(
				$"ARENA VALIDATION | {sectionName} section anchor is missing."
			);

			return;
		}

		// v2.1 expects authoring layouts to live only as prefab assets.
		// Any ArenaLayout child already placed in main.scene is leftover v2.0
		// content and would duplicate the runtime-instantiated copy.
		foreach (
			GameObject child in section.Children
		)
		{
			if (
				child is null ||
				!child.IsValid
			)
				continue;

			ArenaLayout metadata =
				child.GetComponent<ArenaLayout>(
					true
				);

			if ( metadata is null )
				continue;

			warnings++;

			Log.Warning(
				$"ARENA VALIDATION | {sectionName}/{child.Name} is a pre-placed ArenaLayout. Arena v2.1 expects section anchors to be empty in main.scene; remove this instance after adding its prefab to the Catalog."
			);
		}
	}

	private void ValidatePrefabCatalog(
		string label,
		IEnumerable<GameObject> prefabReferences,
		ArenaLayoutSection expectedSection,
		List<LayoutEntry> entries,
		ref int errors,
		ref int warnings
	)
	{
		List<GameObject> refs =
			prefabReferences?
				.Where(
					x =>
						x is not null &&
						x.IsValid
				)
				.ToList()
			?? new List<GameObject>();

		if ( refs.Count == 0 )
		{
			errors++;

			Log.Error(
				$"ARENA VALIDATION | {label} prefab catalog is empty."
			);

			return;
		}

		if ( entries.Count == 0 )
		{
			errors++;

			Log.Error(
				$"ARENA VALIDATION | {label} prefab catalog contains no valid ArenaLayout metadata."
			);

			return;
		}

		foreach (
			GameObject prefab in refs
		)
		{
			ArenaLayout metadata =
				prefab.GetComponent<ArenaLayout>(
					true
				);

			if ( metadata is null )
			{
				errors++;

				Log.Error(
					$"ARENA VALIDATION | {label} prefab reference '{prefab.Name}' has no ArenaLayout component on its root."
				);

				continue;
			}

			if (
				metadata.Section != expectedSection
			)
			{
				errors++;

				Log.Error(
					$"ARENA VALIDATION | {label}/{prefab.Name} is marked Section={metadata.Section}, expected {expectedSection}."
				);
			}

			if ( metadata.LayoutId < 0 )
			{
				errors++;

				Log.Error(
					$"ARENA VALIDATION | {label}/{prefab.Name} uses negative LayoutId {metadata.LayoutId}."
				);
			}

			if (
				WarnAboutNonNormalizedLayoutRoots &&
				!IsNormalizedLayoutRoot(
					prefab
				)
			)
			{
				warnings++;

				Log.Warning(
					$"ARENA VALIDATION | {label}/{prefab.Name} prefab root is not normalized. Expected Local Position 0,0,0; Local Rotation identity; Local Scale 1,1,1."
				);
			}
		}

		foreach (
			IGrouping<int, LayoutEntry> duplicateGroup in
				entries.GroupBy(
					x => x.Id
				)
		)
		{
			if (
				duplicateGroup.Count() <= 1
			)
				continue;

			errors++;

			Log.Error(
				$"ARENA VALIDATION | {label} catalog has duplicate LayoutId {duplicateGroup.Key}."
			);
		}
	}

	private bool IsNormalizedLayoutRoot(
		GameObject root
	)
	{
		if (
			root is null ||
			!root.IsValid
		)
			return false;

		bool positionOkay =
			root.LocalPosition.Distance(
				Vector3.Zero
			) <= 0.01f;

		bool rotationOkay =
			root.LocalRotation.Distance(
				Rotation.Identity
			) <= 0.01f;

		bool scaleOkay =
			root.LocalScale.Distance(
				Vector3.One
			) <= 0.01f;

		return
			positionOkay &&
			rotationOkay &&
			scaleOkay;
	}
}