using System.Collections.Concurrent;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.Interop;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
using IPlayerCharacter = Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;

namespace LightlessSync.Services.ActorTracking;

public sealed class ActorObjectService : IHostedService, IDisposable
{
    public readonly record struct ActorDescriptor(
        string Name,
        string HashedContentId,
        nint Address,
        ushort ObjectIndex,
        bool IsLocalPlayer,
        bool IsInGpose,
        DalamudObjectKind ObjectKind,
        LightlessObjectKind? OwnedKind,
        uint OwnerEntityId);

    private readonly ILogger<ActorObjectService> _logger;
    private readonly IFramework _framework;
    private readonly IGameInteropProvider _interop;
    private readonly IObjectTable _objectTable;
    private readonly IClientState _clientState;
    private readonly ICondition _condition;
    private readonly LightlessMediator _mediator;

    private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
    private readonly ConcurrentDictionary<nint, ActorDescriptor> _gposePlayers = new();
    private readonly ConcurrentDictionary<string, ActorDescriptor> _actorsByHash = new(StringComparer.Ordinal);
    private readonly ConcurrentDictionary<string, ConcurrentDictionary<nint, ActorDescriptor>> _actorsByName = new(StringComparer.Ordinal);
    private readonly ConcurrentDictionary<nint, byte> _pendingHashResolutions = new();
    private ActorSnapshot _snapshot = ActorSnapshot.Empty;
    private GposeSnapshot _gposeSnapshot = GposeSnapshot.Empty;

    private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
    private Hook<Character.Delegates.Terminate>? _onTerminateHook;
    private Hook<Character.Delegates.Dtor>? _onDestructorHook;
    private Hook<Companion.Delegates.OnInitialize>? _onCompanionInitializeHook;
    private Hook<Companion.Delegates.Terminate>? _onCompanionTerminateHook;

    private bool _hooksActive;
    private static readonly TimeSpan SnapshotRefreshInterval = TimeSpan.FromSeconds(1);
    private DateTime _nextRefreshAllowed = DateTime.MinValue;

    public ActorObjectService(
        ILogger<ActorObjectService> logger,
        IFramework framework,
        IGameInteropProvider interop,
        IObjectTable objectTable,
        IClientState clientState,
        ICondition condition,
        LightlessMediator mediator)
    {
        _logger = logger;
        _framework = framework;
        _interop = interop;
        _objectTable = objectTable;
        _clientState = clientState;
        _condition = condition;
        _mediator = mediator;
    }

    private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];

    private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
    private GposeSnapshot CurrentGposeSnapshot => Volatile.Read(ref _gposeSnapshot);

    public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;

    public IEnumerable<ActorDescriptor> ObjectDescriptors => _activePlayers.Values;
    public IReadOnlyList<ActorDescriptor> PlayerDescriptors => Snapshot.PlayerDescriptors;
    public IReadOnlyList<ActorDescriptor> OwnedDescriptors => Snapshot.OwnedDescriptors;
    public IReadOnlyList<ActorDescriptor> GposeDescriptors => CurrentGposeSnapshot.GposeDescriptors;

    public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
    public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
    {
        descriptor = default;
        if (!_actorsByHash.TryGetValue(hash, out var candidate))
            return false;

        if (!ValidateDescriptorThreadSafe(candidate))
            return false;

        descriptor = candidate;
        return true;
    }

    public bool TryGetPlayerByName(string name, out ActorDescriptor descriptor)
    {
        descriptor = default;

        if (!_actorsByName.TryGetValue(name, out var entries) || entries.IsEmpty)
            return false;

        ActorDescriptor? best = null;
        foreach (var candidate in entries.Values)
        {
            if (!ValidateDescriptorThreadSafe(candidate))
                continue;

            if (best is null || IsBetterNameMatch(candidate, best.Value))
            {
                best = candidate;
            }
        }

        if (best is { } selected)
        {
            descriptor = selected;
            return true;
        }

        return false;
    }
    public bool HooksActive => _hooksActive;
    public bool HasPendingHashResolutions => !_pendingHashResolutions.IsEmpty;
    public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers;
    public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions;
    public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses;
    public IReadOnlyDictionary<nint, LightlessObjectKind> OwnedObjects => Snapshot.OwnedObjects.Map;
    public nint LocalPlayerAddress => Snapshot.OwnedObjects.LocalPlayer;
    public nint LocalPetAddress => Snapshot.OwnedObjects.LocalPet;
    public nint LocalMinionOrMountAddress => Snapshot.OwnedObjects.LocalMinionOrMount;
    public nint LocalCompanionAddress => Snapshot.OwnedObjects.LocalCompanion;

    public bool TryGetOwnedKind(nint address, out LightlessObjectKind kind)
        => OwnedObjects.TryGetValue(address, out kind);

    public bool TryGetOwnedActor(LightlessObjectKind kind, out ActorDescriptor descriptor)
    {
        descriptor = default;
        if (!TryGetOwnedObject(kind, out var address))
            return false;
        return TryGetDescriptor(address, out descriptor);
    }

    public bool TryGetOwnedObjectByIndex(ushort objectIndex, out LightlessObjectKind ownedKind)
    {
        ownedKind = default;
        var ownedDescriptors = OwnedDescriptors;
        for (var i = 0; i < ownedDescriptors.Count; i++)
        {
            var descriptor = ownedDescriptors[i];
            if (descriptor.ObjectIndex != objectIndex)
                continue;

            if (descriptor.OwnedKind is { } resolvedKind)
            {
                ownedKind = resolvedKind;
                return true;
            }
        }

        return false;
    }

    public bool TryGetOwnedObject(LightlessObjectKind kind, out nint address)
    {
        var ownedSnapshot = Snapshot.OwnedObjects;
        address = kind switch
        {
            LightlessObjectKind.Player => ownedSnapshot.LocalPlayer,
            LightlessObjectKind.Pet => ownedSnapshot.LocalPet,
            LightlessObjectKind.MinionOrMount => ownedSnapshot.LocalMinionOrMount,
            LightlessObjectKind.Companion => ownedSnapshot.LocalCompanion,
            _ => nint.Zero
        };

        return address != nint.Zero;
    }

    public bool TryGetOwnedActor(uint ownerEntityId, LightlessObjectKind? kindFilter, out ActorDescriptor descriptor)
    {
        descriptor = default;
        foreach (var candidate in _activePlayers.Values)
        {
            if (candidate.OwnerEntityId != ownerEntityId)
                continue;

            if (kindFilter.HasValue && candidate.OwnedKind != kindFilter)
                continue;

            descriptor = candidate;
            return true;
        }

        return false;
    }

    public bool TryGetPlayerAddressByHash(string hash, out nint address)
    {
        if (TryGetValidatedActorByHash(hash, out var descriptor) && descriptor.Address != nint.Zero)
        {
            address = descriptor.Address;
            return true;
        }

        address = nint.Zero;
        return false;
    }

    public async Task WaitForFullyLoadedAsync(nint address, CancellationToken cancellationToken = default)
    {
        if (address == nint.Zero)
            throw new ArgumentException("Address cannot be zero.", nameof(address));

        while (true)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
            if (!IsZoning && isLoaded)
                return;

            await Task.Delay(100, cancellationToken).ConfigureAwait(false);
        }
    }

    private bool ValidateDescriptorThreadSafe(ActorDescriptor descriptor)
    {
        if (_framework.IsInFrameworkUpdateThread)
            return ValidateDescriptorInternal(descriptor);

        return _framework.RunOnFrameworkThread(() => ValidateDescriptorInternal(descriptor)).GetAwaiter().GetResult();
    }

    private bool ValidateDescriptorInternal(ActorDescriptor descriptor)
    {
        if (descriptor.Address == nint.Zero)
            return false;

        if (descriptor.ObjectKind == DalamudObjectKind.Player &&
            !string.IsNullOrEmpty(descriptor.HashedContentId))
        {
            if (!TryGetLivePlayerHash(descriptor, out var liveHash))
            {
                UntrackGameObject(descriptor.Address);
                return false;
            }

            if (!string.Equals(liveHash, descriptor.HashedContentId, StringComparison.Ordinal))
            {
                UntrackGameObject(descriptor.Address);
                return false;
            }
        }

        return true;
    }

    private bool TryGetLivePlayerHash(ActorDescriptor descriptor, out string liveHash)
    {
        liveHash = string.Empty;

        if (_objectTable.CreateObjectReference(descriptor.Address) is not IPlayerCharacter playerCharacter)
            return false;

        return DalamudUtilService.TryGetHashedCID(playerCharacter, out liveHash);
    }

    public void RefreshTrackedActors(bool force = false)
    {
        var now = DateTime.UtcNow;
        if (!force && _hooksActive)
        {
            if (now < _nextRefreshAllowed)
                return;

            _nextRefreshAllowed = now + SnapshotRefreshInterval;
        }

        if (_framework.IsInFrameworkUpdateThread)
        {
            RefreshTrackedActorsInternal();
        }
        else
        {
            _ = _framework.RunOnFrameworkThread(RefreshTrackedActorsInternal);
        }
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        try
        {
            InitializeHooks();
            var warmupTask = WarmupExistingActors();
            return warmupTask;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to initialize ActorObjectService hooks, falling back to empty cache.");
            DisposeHooks();
            return Task.CompletedTask;
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        DisposeHooks();
        _activePlayers.Clear();
        _gposePlayers.Clear();
        _actorsByHash.Clear();
        _actorsByName.Clear();
        _pendingHashResolutions.Clear();
        Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
        Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
        return Task.CompletedTask;
    }

    private unsafe void InitializeHooks()
    {
        if (_hooksActive)
            return;

        _onInitializeHook = _interop.HookFromAddress<Character.Delegates.OnInitialize>(
            (nint)Character.StaticVirtualTablePointer->OnInitialize,
            OnCharacterInitialized);

        _onTerminateHook = _interop.HookFromAddress<Character.Delegates.Terminate>(
            (nint)Character.StaticVirtualTablePointer->Terminate,
            OnCharacterTerminated);

        _onDestructorHook = _interop.HookFromAddress<Character.Delegates.Dtor>(
            (nint)Character.StaticVirtualTablePointer->Dtor,
            OnCharacterDisposed);

        _onCompanionInitializeHook = _interop.HookFromAddress<Companion.Delegates.OnInitialize>(
            (nint)Companion.StaticVirtualTablePointer->OnInitialize,
            OnCompanionInitialized);

        _onCompanionTerminateHook = _interop.HookFromAddress<Companion.Delegates.Terminate>(
            (nint)Companion.StaticVirtualTablePointer->Terminate,
            OnCompanionTerminated);

        _onInitializeHook.Enable();
        _onTerminateHook.Enable();
        _onDestructorHook.Enable();
        _onCompanionInitializeHook.Enable();
        _onCompanionTerminateHook.Enable();

        _hooksActive = true;
        _logger.LogTrace("ActorObjectService hooks enabled.");
    }

    private Task WarmupExistingActors()
    {
        return _framework.RunOnFrameworkThread(() =>
        {
            RefreshTrackedActorsInternal();
            _nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval;
        });
    }

    private unsafe void OnCharacterInitialized(Character* chara)
    {
        ExecuteOriginal(() => _onInitializeHook!.Original(chara), "Error invoking original character initialize.");
        QueueTrack((GameObject*)chara);
    }

    private unsafe void OnCharacterTerminated(Character* chara)
    {
        var address = (nint)chara;
        QueueUntrack(address);
        ExecuteOriginal(() => _onTerminateHook!.Original(chara), "Error invoking original character terminate.");
    }

    private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
    {
        var address = (nint)chara;
        QueueUntrack(address);
        try
        {
            return _onDestructorHook!.Original(chara, freeMemory);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error invoking original character destructor.");
            return null;
        }
    }

    private unsafe void TrackGameObject(GameObject* gameObject)
    {
        if (gameObject == null)
            return;

        var objectKind = (DalamudObjectKind)gameObject->ObjectKind;

        if (!IsSupportedObjectKind(objectKind))
            return;

        if (BuildDescriptor(gameObject, objectKind) is not { } descriptor)
            return;

        if (descriptor.ObjectKind != DalamudObjectKind.Player && descriptor.OwnedKind is null)
            return;

        if (_activePlayers.TryGetValue(descriptor.Address, out var existing))
        {
            RemoveDescriptor(existing);
        }

        AddDescriptor(descriptor);

        if (_logger.IsEnabled(LogLevel.Debug))
        {
            _logger.LogTrace("Actor tracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind} local={Local} gpose={Gpose}",
                descriptor.Name,
                descriptor.Address,
                descriptor.ObjectIndex,
                descriptor.OwnedKind?.ToString() ?? "<none>",
                descriptor.IsLocalPlayer,
                descriptor.IsInGpose);
        }

        _mediator.Publish(new ActorTrackedMessage(descriptor));
    }

    private unsafe ActorDescriptor? BuildDescriptor(GameObject* gameObject, DalamudObjectKind objectKind)
    {
        if (gameObject == null)
            return null;

        var address = (nint)gameObject;
        string name = string.Empty;
        ushort objectIndex = gameObject->ObjectIndex;
        bool isInGpose = objectIndex >= 200;
        bool isLocal = _objectTable.LocalPlayer?.Address == address;
        string hashedCid = string.Empty;

        IPlayerCharacter? resolvedPlayer = null;
        if (_objectTable.CreateObjectReference(address) is IPlayerCharacter playerCharacter)
        {
            resolvedPlayer = playerCharacter;
            name = playerCharacter.Name.TextValue ?? string.Empty;
            objectIndex = playerCharacter.ObjectIndex;
            isInGpose = objectIndex >= 200;
            isLocal = playerCharacter.Address == _objectTable.LocalPlayer?.Address;
        }
        else
        {
            name = gameObject->NameString ?? string.Empty;
        }

        if (objectKind == DalamudObjectKind.Player)
        {
            if (resolvedPlayer == null || !DalamudUtilService.TryGetHashedCID(resolvedPlayer, out hashedCid))
            {
                hashedCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address);
            }
        }

        var (ownedKind, ownerEntityId) = DetermineOwnedKind(gameObject, objectKind, isLocal);

        return new ActorDescriptor(name, hashedCid, address, objectIndex, isLocal, isInGpose, objectKind, ownedKind, ownerEntityId);
    }

    private unsafe (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(GameObject* gameObject, DalamudObjectKind objectKind, bool isLocalPlayer)
    {
        if (gameObject == null)
            return (null, 0);

        if (objectKind == DalamudObjectKind.Player)
        {
            var entityId = ((Character*)gameObject)->EntityId;
            return (isLocalPlayer ? LightlessObjectKind.Player : null, entityId);
        }

        var ownerId = ResolveOwnerId(gameObject);
        var localPlayerAddress = _objectTable.LocalPlayer?.Address ?? nint.Zero;
        if (localPlayerAddress == nint.Zero)
            return (null, ownerId);

        var localEntityId = ((Character*)localPlayerAddress)->EntityId;
        if (localEntityId == 0)
            return (null, ownerId);

        if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
        {
            var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId);
            if (expectedMinionOrMount != nint.Zero && (nint)gameObject == expectedMinionOrMount)
            {
                var resolvedOwner = ownerId != 0 ? ownerId : localEntityId;
                return (LightlessObjectKind.MinionOrMount, resolvedOwner);
            }
        }

        if (objectKind != DalamudObjectKind.BattleNpc)
            return (null, ownerId);

        if (ownerId != localEntityId)
            return (null, ownerId);

        var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
        if (expectedPet != nint.Zero && (nint)gameObject == expectedPet)
            return (LightlessObjectKind.Pet, ownerId);

        var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
        if (expectedCompanion != nint.Zero && (nint)gameObject == expectedCompanion)
            return (LightlessObjectKind.Companion, ownerId);

        return (null, ownerId);
    }

    private unsafe nint GetMinionOrMountAddress(nint localPlayerAddress, uint ownerEntityId)
    {
        if (localPlayerAddress == nint.Zero)
            return nint.Zero;

        var playerObject = (GameObject*)localPlayerAddress;
        var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
        if (candidateAddress != nint.Zero)
        {
            var candidate = (GameObject*)candidateAddress;
            var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
            if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
            {
                if (ownerEntityId == 0 || ResolveOwnerId(candidate) == ownerEntityId)
                    return candidateAddress;
            }
        }

        if (ownerEntityId == 0)
            return candidateAddress;

        foreach (var obj in _objectTable)
        {
            if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
                continue;

            if (obj.ObjectKind is not (DalamudObjectKind.MountType or DalamudObjectKind.Companion))
                continue;

            var candidate = (GameObject*)obj.Address;
            if (ResolveOwnerId(candidate) == ownerEntityId)
                return obj.Address;
        }

        return candidateAddress;
    }

    private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId)
    {
        if (localPlayerAddress == nint.Zero || ownerEntityId == 0)
            return nint.Zero;

        var manager = CharacterManager.Instance();
        if (manager != null)
        {
            var candidate = (nint)manager->LookupPetByOwnerObject((BattleChara*)localPlayerAddress);
            if (candidate != nint.Zero)
            {
                var candidateObj = (GameObject*)candidate;
                if (IsPetMatch(candidateObj, ownerEntityId))
                    return candidate;
            }
        }

        foreach (var obj in _objectTable)
        {
            if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
                continue;

            if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
                continue;

            var candidate = (GameObject*)obj.Address;
            if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
                continue;

            if (ResolveOwnerId(candidate) == ownerEntityId)
                return obj.Address;
        }

        return nint.Zero;
    }

    private unsafe nint GetCompanionAddress(nint localPlayerAddress, uint ownerEntityId)
    {
        if (localPlayerAddress == nint.Zero || ownerEntityId == 0)
            return nint.Zero;

        var manager = CharacterManager.Instance();
        if (manager != null)
        {
            var candidate = (nint)manager->LookupBuddyByOwnerObject((BattleChara*)localPlayerAddress);
            if (candidate != nint.Zero)
            {
                var candidateObj = (GameObject*)candidate;
                if (IsCompanionMatch(candidateObj, ownerEntityId))
                    return candidate;
            }
        }

        foreach (var obj in _objectTable)
        {
            if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
                continue;

            if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
                continue;

            var candidate = (GameObject*)obj.Address;
            if (candidate->BattleNpcSubKind != BattleNpcSubKind.Buddy)
                continue;

            if (ResolveOwnerId(candidate) == ownerEntityId)
                return obj.Address;
        }

        return nint.Zero;
    }

    private static unsafe bool IsPetMatch(GameObject* candidate, uint ownerEntityId)
    {
        if (candidate == null)
            return false;

        if ((DalamudObjectKind)candidate->ObjectKind != DalamudObjectKind.BattleNpc)
            return false;

        if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
            return false;

        return ResolveOwnerId(candidate) == ownerEntityId;
    }

    private static unsafe bool IsCompanionMatch(GameObject* candidate, uint ownerEntityId)
    {
        if (candidate == null)
            return false;

        if ((DalamudObjectKind)candidate->ObjectKind != DalamudObjectKind.BattleNpc)
            return false;

        if (candidate->BattleNpcSubKind != BattleNpcSubKind.Buddy)
            return false;

        return ResolveOwnerId(candidate) == ownerEntityId;
    }

    private static unsafe uint ResolveOwnerId(GameObject* gameObject)
    {
        if (gameObject == null)
            return 0;

        if (gameObject->OwnerId != 0)
            return gameObject->OwnerId;

        var character = (Character*)gameObject;
        if (character == null)
            return 0;

        if (character->CompanionOwnerId != 0)
            return character->CompanionOwnerId;

        var parent = character->GetParentCharacter();
        return parent != null ? parent->EntityId : 0;
    }

    private void UntrackGameObject(nint address)
    {
        if (address == nint.Zero)
            return;

        if (_activePlayers.TryRemove(address, out var descriptor))
        {
            RemoveDescriptor(descriptor);
            if (_logger.IsEnabled(LogLevel.Debug))
            {
                _logger.LogTrace("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}",
                    descriptor.Name,
                    descriptor.Address,
                    descriptor.ObjectIndex,
                    descriptor.OwnedKind?.ToString() ?? "<none>");
            }

            _mediator.Publish(new ActorUntrackedMessage(descriptor));
        }
    }

    private unsafe void RefreshTrackedActorsInternal()
    {
        var addresses = EnumerateActiveCharacterAddresses();
        HashSet<nint> seen = new(addresses.Count);

        foreach (var address in addresses)
        {
            if (address == nint.Zero)
                continue;

            if (!seen.Add(address))
                continue;

            var gameObject = (GameObject*)address;
            if (_activePlayers.TryGetValue(address, out var existing))
            {
                RefreshDescriptorIfNeeded(existing, gameObject);
                continue;
            }

            TrackGameObject(gameObject);
        }

        var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
        foreach (var staleAddress in stale)
        {
            UntrackGameObject(staleAddress);
        }

        if (_hooksActive)
        {
            _nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval;
        }

        if (_clientState.IsGPosing)
        {
            RefreshGposeActorsInternal();
        }
        else if (!_gposePlayers.IsEmpty)
        {
            _gposePlayers.Clear();
            PublishGposeSnapshot();
        }
    }

    private unsafe void RefreshDescriptorIfNeeded(ActorDescriptor existing, GameObject* gameObject)
    {
        if (gameObject == null)
            return;

        if (existing.ObjectKind != DalamudObjectKind.Player || !string.IsNullOrEmpty(existing.HashedContentId))
            return;

        var objectKind = (DalamudObjectKind)gameObject->ObjectKind;
        if (!IsSupportedObjectKind(objectKind))
            return;

        if (BuildDescriptor(gameObject, objectKind) is not { } updated)
            return;

        if (string.IsNullOrEmpty(updated.HashedContentId))
            return;

        ReplaceDescriptor(existing, updated);
        _mediator.Publish(new ActorTrackedMessage(updated));
    }

    private void ReplaceDescriptor(ActorDescriptor existing, ActorDescriptor updated)
    {
        RemoveDescriptorFromIndexes(existing);
        _activePlayers[updated.Address] = updated;
        IndexDescriptor(updated);
        UpdatePendingHashResolutions(updated);
        PublishSnapshot();
    }

    private void IndexDescriptor(ActorDescriptor descriptor)
    {
        if (!string.IsNullOrEmpty(descriptor.HashedContentId))
        {
            _actorsByHash[descriptor.HashedContentId] = descriptor;
        }

        if (descriptor.ObjectKind == DalamudObjectKind.Player && !string.IsNullOrEmpty(descriptor.Name))
        {
            var bucket = _actorsByName.GetOrAdd(descriptor.Name, _ => new ConcurrentDictionary<nint, ActorDescriptor>());
            bucket[descriptor.Address] = descriptor;
        }
    }

    private static bool IsBetterNameMatch(ActorDescriptor candidate, ActorDescriptor current)
    {
        if (!candidate.IsInGpose && current.IsInGpose)
            return true;
        if (candidate.IsInGpose && !current.IsInGpose)
            return false;

        return candidate.ObjectIndex < current.ObjectIndex;
    }

    private bool TryGetDescriptor(nint address, out ActorDescriptor descriptor)
        => _activePlayers.TryGetValue(address, out descriptor);

    private unsafe void OnCompanionInitialized(Companion* companion)
    {
        ExecuteOriginal(() => _onCompanionInitializeHook!.Original(companion), "Error invoking original companion initialize.");
        QueueTrack((GameObject*)companion);
    }

    private unsafe void OnCompanionTerminated(Companion* companion)
    {
        var address = (nint)companion;
        QueueUntrack(address);
        ExecuteOriginal(() => _onCompanionTerminateHook!.Original(companion), "Error invoking original companion terminate.");
    }

    private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor)
    {
        if (!string.IsNullOrEmpty(descriptor.HashedContentId))
        {
            _actorsByHash.TryRemove(descriptor.HashedContentId, out _);
        }

        if (descriptor.ObjectKind == DalamudObjectKind.Player
            && !string.IsNullOrEmpty(descriptor.Name)
            && _actorsByName.TryGetValue(descriptor.Name, out var bucket))
        {
            bucket.TryRemove(descriptor.Address, out _);
            if (bucket.IsEmpty)
            {
                _actorsByName.TryRemove(descriptor.Name, out _);
            }
        }
    }

    private void AddDescriptor(ActorDescriptor descriptor)
    {
        _activePlayers[descriptor.Address] = descriptor;
        IndexDescriptor(descriptor);
        UpdatePendingHashResolutions(descriptor);
        PublishSnapshot();
    }

    private void RemoveDescriptor(ActorDescriptor descriptor)
    {
        RemoveDescriptorFromIndexes(descriptor);
        _pendingHashResolutions.TryRemove(descriptor.Address, out _);
        PublishSnapshot();
    }

    private void UpdatePendingHashResolutions(ActorDescriptor descriptor)
    {
        if (descriptor.ObjectKind != DalamudObjectKind.Player)
        {
            _pendingHashResolutions.TryRemove(descriptor.Address, out _);
            return;
        }

        if (string.IsNullOrEmpty(descriptor.HashedContentId))
        {
            _pendingHashResolutions[descriptor.Address] = 1;
            return;
        }

        _pendingHashResolutions.TryRemove(descriptor.Address, out _);
    }

    private void PublishSnapshot()
    {
        var descriptors = _activePlayers.Values.ToArray();
        var playerCount = 0;
        var ownedCount = 0;
        var companionCount = 0;

        foreach (var descriptor in descriptors)
        {
            if (descriptor.ObjectKind == DalamudObjectKind.Player)
                playerCount++;

            if (descriptor.OwnedKind is not null)
                ownedCount++;

            if (descriptor.ObjectKind == DalamudObjectKind.Companion)
                companionCount++;
        }

        var playerDescriptors = new ActorDescriptor[playerCount];
        var ownedDescriptors = new ActorDescriptor[ownedCount];
        var playerAddresses = new nint[playerCount];
        var renderedCompanions = new nint[companionCount];
        var ownedAddresses = new nint[ownedCount];
        var ownedMap = new Dictionary<nint, LightlessObjectKind>(ownedCount);
        nint localPlayer = nint.Zero;
        nint localPet = nint.Zero;
        nint localMinionOrMount = nint.Zero;
        nint localCompanion = nint.Zero;

        var playerIndex = 0;
        var ownedIndex = 0;
        var companionIndex = 0;

        foreach (var descriptor in descriptors)
        {
            if (descriptor.ObjectKind == DalamudObjectKind.Player)
            {
                playerDescriptors[playerIndex] = descriptor;
                playerAddresses[playerIndex] = descriptor.Address;
                playerIndex++;
            }

            if (descriptor.ObjectKind == DalamudObjectKind.Companion)
            {
                renderedCompanions[companionIndex] = descriptor.Address;
                companionIndex++;
            }

            if (descriptor.OwnedKind is not { } ownedKind)
            {
                continue;
            }

            ownedDescriptors[ownedIndex] = descriptor;
            ownedAddresses[ownedIndex] = descriptor.Address;
            ownedMap[descriptor.Address] = ownedKind;

            switch (ownedKind)
            {
                case LightlessObjectKind.Player:
                    localPlayer = descriptor.Address;
                    break;
                case LightlessObjectKind.Pet:
                    localPet = descriptor.Address;
                    break;
                case LightlessObjectKind.MinionOrMount:
                    localMinionOrMount = descriptor.Address;
                    break;
                case LightlessObjectKind.Companion:
                    localCompanion = descriptor.Address;
                    break;
            }

            ownedIndex++;
        }

        var ownedSnapshot = new OwnedObjectSnapshot(
            playerAddresses,
            renderedCompanions,
            ownedAddresses,
            ownedMap,
            localPlayer,
            localPet,
            localMinionOrMount,
            localCompanion);
        var nextGeneration = Snapshot.Generation + 1;
        var snapshot = new ActorSnapshot(playerDescriptors, ownedDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
        Volatile.Write(ref _snapshot, snapshot);
    }

    private void QueueFrameworkUpdate(Action action)
    {
        if (action == null)
            return;

        if (_framework.IsInFrameworkUpdateThread)
        {
            action();
            return;
        }

        _ = _framework.RunOnFrameworkThread(action);
    }

    private void ExecuteOriginal(Action action, string errorMessage)
    {
        try
        {
            action();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, errorMessage);
        }
    }

    private unsafe void QueueTrack(GameObject* gameObject)
        => QueueFrameworkUpdate(() => TrackGameObject(gameObject));

    private void QueueUntrack(nint address)
        => QueueFrameworkUpdate(() => UntrackGameObject(address));

    private void DisposeHooks()
    {
        var hadHooks = _hooksActive
            || _onInitializeHook is not null
            || _onTerminateHook is not null
            || _onDestructorHook is not null
            || _onCompanionInitializeHook is not null
            || _onCompanionTerminateHook is not null;

        _onInitializeHook?.Disable();
        _onTerminateHook?.Disable();
        _onDestructorHook?.Disable();
        _onCompanionInitializeHook?.Disable();
        _onCompanionTerminateHook?.Disable();

        _onInitializeHook?.Dispose();
        _onTerminateHook?.Dispose();
        _onDestructorHook?.Dispose();
        _onCompanionInitializeHook?.Dispose();
        _onCompanionTerminateHook?.Dispose();

        _onInitializeHook = null;
        _onTerminateHook = null;
        _onDestructorHook = null;
        _onCompanionInitializeHook = null;
        _onCompanionTerminateHook = null;

        _hooksActive = false;

        if (hadHooks)
        {
            _logger.LogTrace("ActorObjectService hooks disabled.");
        }
    }

    public void Dispose()
    {
        DisposeHooks();
        GC.SuppressFinalize(this);
    }

    private static bool IsSupportedObjectKind(DalamudObjectKind objectKind) =>
        objectKind is DalamudObjectKind.Player
            or DalamudObjectKind.BattleNpc
            or DalamudObjectKind.Companion
            or DalamudObjectKind.MountType;

    private static unsafe List<nint> EnumerateActiveCharacterAddresses()
    {
        var results = new List<nint>(64);
        var manager = GameObjectManager.Instance();
        if (manager == null)
            return results;

        const int objectLimit = 200;

        unsafe
        {
            for (var i = 0; i < objectLimit; i++)
            {
                Pointer<GameObject> objPtr = manager->Objects.IndexSorted[i];
                var obj = objPtr.Value;
                if (obj == null)
                    continue;

                var objectKind = (DalamudObjectKind)obj->ObjectKind;
                if (!IsSupportedObjectKind(objectKind))
                    continue;

                results.Add((nint)obj);
            }
        }

        return results;
    }

    private unsafe void RefreshGposeActorsInternal()
    {
        var addresses = EnumerateGposeCharacterAddresses();
        HashSet<nint> seen = new(addresses.Count);

        foreach (var address in addresses)
        {
            if (address == nint.Zero)
                continue;

            if (!seen.Add(address))
                continue;

            if (_gposePlayers.ContainsKey(address))
                continue;

            TrackGposeObject((GameObject*)address);
        }

        var stale = _gposePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
        foreach (var staleAddress in stale)
        {
            UntrackGposeObject(staleAddress);
        }

        PublishGposeSnapshot();
    }

    private unsafe void TrackGposeObject(GameObject* gameObject)
    {
        if (gameObject == null)
            return;

        var objectKind = (DalamudObjectKind)gameObject->ObjectKind;
        if (objectKind != DalamudObjectKind.Player)
            return;

        if (BuildDescriptor(gameObject, objectKind) is not { } descriptor)
            return;

        if (!descriptor.IsInGpose)
            return;

        _gposePlayers[descriptor.Address] = descriptor;
    }

    private void UntrackGposeObject(nint address)
    {
        if (address == nint.Zero)
            return;

        _gposePlayers.TryRemove(address, out _);
    }

    private void PublishGposeSnapshot()
    {
        var gposeDescriptors = _gposePlayers.Values.ToArray();
        var gposeAddresses = new nint[gposeDescriptors.Length];
        for (var i = 0; i < gposeDescriptors.Length; i++)
            gposeAddresses[i] = gposeDescriptors[i].Address;

        var nextGeneration = CurrentGposeSnapshot.Generation + 1;
        var snapshot = new GposeSnapshot(gposeDescriptors, gposeAddresses, nextGeneration);
        Volatile.Write(ref _gposeSnapshot, snapshot);
    }

    private List<nint> EnumerateGposeCharacterAddresses()
    {
        var results = new List<nint>(16);
        foreach (var obj in _objectTable)
        {
            if (obj.ObjectKind != DalamudObjectKind.Player)
                continue;

            if (obj.ObjectIndex < 200)
                continue;

            results.Add(obj.Address);
        }

        return results;
    }

    private static unsafe bool IsObjectFullyLoaded(nint address)
    {
        if (address == nint.Zero)
            return false;

        var gameObject = (GameObject*)address;
        if (gameObject == null)
            return false;

        var drawObject = gameObject->DrawObject;
        if (drawObject == null)
            return false;

        if ((ulong)gameObject->RenderFlags == 2048)
            return false;

        var characterBase = (CharacterBase*)drawObject;
        if (characterBase->HasModelInSlotLoaded != 0)
            return false;

        if (characterBase->HasModelFilesInSlotLoaded != 0)
            return false;

        return true;
    }

    private sealed record OwnedObjectSnapshot(
        IReadOnlyList<nint> RenderedPlayers,
        IReadOnlyList<nint> RenderedCompanions,
        IReadOnlyList<nint> OwnedAddresses,
        IReadOnlyDictionary<nint, LightlessObjectKind> Map,
        nint LocalPlayer,
        nint LocalPet,
        nint LocalMinionOrMount,
        nint LocalCompanion)
    {
        public static OwnedObjectSnapshot Empty { get; } = new(
            Array.Empty<nint>(),
            Array.Empty<nint>(),
            Array.Empty<nint>(),
            new Dictionary<nint, LightlessObjectKind>(),
            nint.Zero,
            nint.Zero,
            nint.Zero,
            nint.Zero);
    }

    private sealed record ActorSnapshot(
        IReadOnlyList<ActorDescriptor> PlayerDescriptors,
        IReadOnlyList<ActorDescriptor> OwnedDescriptors,
        IReadOnlyList<nint> PlayerAddresses,
        OwnedObjectSnapshot OwnedObjects,
        int Generation)
    {
        public static ActorSnapshot Empty { get; } = new(
            Array.Empty<ActorDescriptor>(),
            Array.Empty<ActorDescriptor>(),
            Array.Empty<nint>(),
            OwnedObjectSnapshot.Empty,
            0);
    }

    private sealed record GposeSnapshot(
        IReadOnlyList<ActorDescriptor> GposeDescriptors,
        IReadOnlyList<nint> GposeAddresses,
        int Generation)
    {
        public static GposeSnapshot Empty { get; } = new(
            Array.Empty<ActorDescriptor>(),
            Array.Empty<nint>(),
            0);
    }
}
