using LightlessSync.API.Dto.Chat;
using LightlessSync.API.Data.Extensions;
using LightlessSync.Services.ActorTracking;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using LightlessSync.UI.Services;
using LightlessSync.LightlessConfiguration;

namespace LightlessSync.Services.Chat;

public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedService
{
    private const int MaxMessageHistory = 150;
    internal const int MaxOutgoingLength = 200;
    private const int MaxUnreadCount = 999;
    private const string ZoneUnavailableMessage = "Zone chat is only available in major cities.";
    private const string ZoneChannelKey = "zone";
    private const int MaxReportReasonLength = 100;
    private const int MaxReportContextLength = 1000;

    private readonly ApiController _apiController;
    private readonly DalamudUtilService _dalamudUtilService;
    private readonly ActorObjectService _actorObjectService;
    private readonly PairUiService _pairUiService;
    private readonly ChatConfigService _chatConfigService;
    private readonly ServerConfigurationManager _serverConfigurationManager;

    private readonly Lock _sync = new();

    private readonly Dictionary<string, ChatChannelState> _channels = new(StringComparer.Ordinal);
    private readonly List<string> _channelOrder = new();
    private readonly Dictionary<uint, string> _territoryToZoneKey = new();
    private readonly Dictionary<string, ZoneChannelDefinition> _zoneDefinitions = new(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<string, GroupChannelDefinition> _groupDefinitions = new(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<string, int> _lastReadCounts = new(StringComparer.Ordinal);
    private readonly Dictionary<string, bool> _lastPresenceStates = new(StringComparer.Ordinal);
    private readonly Dictionary<string, string> _selfTokens = new(StringComparer.Ordinal);
    private readonly List<PendingSelfMessage> _pendingSelfMessages = new();
    private readonly Dictionary<string, List<ChatMessageEntry>> _messageHistoryCache = new(StringComparer.Ordinal);
    private List<ChatChannelSnapshot>? _cachedChannelSnapshots;
    private bool _channelsSnapshotDirty = true;

    private bool _isLoggedIn;
    private bool _isConnected;
    private ChatChannelDescriptor? _lastZoneDescriptor;
    private string? _activeChannelKey;
    private bool _chatEnabled = false;
    private bool _chatHandlerRegistered;

    public ZoneChatService(
        ILogger<ZoneChatService> logger,
        LightlessMediator mediator,
        ChatConfigService chatConfigService,
        ApiController apiController,
        DalamudUtilService dalamudUtilService,
        ActorObjectService actorObjectService,
        PairUiService pairUiService,
        ServerConfigurationManager serverConfigurationManager)
        : base(logger, mediator)
    {
        _apiController = apiController;
        _dalamudUtilService = dalamudUtilService;
        _actorObjectService = actorObjectService;
        _pairUiService = pairUiService;
        _chatConfigService = chatConfigService;
        _serverConfigurationManager = serverConfigurationManager;

        _isLoggedIn = _dalamudUtilService.IsLoggedIn;
        _isConnected = _apiController.IsConnected;
        _chatEnabled = chatConfigService.Current.AutoEnableChatOnLogin;
    }

    public IReadOnlyList<ChatChannelSnapshot> GetChannelsSnapshot()
    {
        using (_sync.EnterScope())
        {
            if (!_channelsSnapshotDirty && _cachedChannelSnapshots is not null)
            {
                return _cachedChannelSnapshots;
            }

            var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count);
            foreach (var key in _channelOrder)
            {
                if (!_channels.TryGetValue(key, out var state))
                    continue;

                var statusText = state.StatusText;
                if (!_chatEnabled)
                {
                    statusText = "Chat services disabled";
                }
                else if (!_isConnected)
                {
                    statusText = "Disconnected from chat server";
                }

                snapshots.Add(new ChatChannelSnapshot(
                    state.Key,
                    state.Descriptor,
                    state.DisplayName,
                    state.Type,
                    state.IsConnected,
                    state.IsConnected && state.IsAvailable,
                    statusText,
                    state.HasUnread,
                    state.UnreadCount,
                    state.Messages.ToList()));
            }

            _cachedChannelSnapshots = snapshots;
            _channelsSnapshotDirty = false;
            return snapshots;
        }
    }

    public bool IsChatEnabled
    {
        get
        {
            using (_sync.EnterScope())
            {
                return _chatEnabled;
            }
        }
    }

    public bool IsChatConnected
    {
        get
        {
            using (_sync.EnterScope())
            {
                return _chatEnabled && _isConnected;
            }
        }
    }

    public void SetActiveChannel(string? key)
    {
        using (_sync.EnterScope())
        {
            _activeChannelKey = key;
            if (key is not null && _channels.TryGetValue(key, out var state))
            {
                state.HasUnread = false;
                state.UnreadCount = 0;
                _lastReadCounts[key] = state.Messages.Count;
            }

            MarkChannelsSnapshotDirtyLocked();
        }
    }

    public void MoveChannel(string draggedKey, string targetKey)
    {
        if (string.IsNullOrWhiteSpace(draggedKey) || string.IsNullOrWhiteSpace(targetKey))
        {
            return;
        }

        bool updated = false;
        using (_sync.EnterScope())
        {
            if (!_channels.ContainsKey(draggedKey) || !_channels.ContainsKey(targetKey))
            {
                return;
            }

            var fromIndex = _channelOrder.IndexOf(draggedKey);
            var toIndex = _channelOrder.IndexOf(targetKey);
            if (fromIndex < 0 || toIndex < 0 || fromIndex == toIndex)
            {
                return;
            }

            _channelOrder.RemoveAt(fromIndex);
            var insertIndex = Math.Clamp(toIndex, 0, _channelOrder.Count);
            _channelOrder.Insert(insertIndex, draggedKey);
            _chatConfigService.Current.ChannelOrder = new List<string>(_channelOrder);
            _chatConfigService.Save();
            updated = true;
        }

        if (updated)
        {
            PublishChannelListChanged();
        }
    }

    public Task SetChatEnabledAsync(bool enabled)
        => enabled ? EnableChatAsync() : DisableChatAsync();

    private async Task EnableChatAsync()
    {
        bool wasEnabled;
        using (_sync.EnterScope())
        {
            wasEnabled = _chatEnabled;
            if (!wasEnabled)
            {
                _chatEnabled = true;
                MarkChannelsSnapshotDirtyLocked();
            }
        }

        if (wasEnabled)
            return;

        RegisterChatHandler();

        await RefreshChatChannelDefinitionsAsync().ConfigureAwait(false);
        ScheduleZonePresenceUpdate(force: true);
        await EnsureGroupPresenceAsync(force: true).ConfigureAwait(false);
    }

    private async Task DisableChatAsync()
    {
        bool wasEnabled;
        List<ChatChannelDescriptor> groupDescriptors;
        ChatChannelDescriptor? zoneDescriptor;

        using (_sync.EnterScope())
        {
            wasEnabled = _chatEnabled;
            if (!wasEnabled)
            {
                return;
            }

            _chatEnabled = false;
            zoneDescriptor = _lastZoneDescriptor;
            _lastZoneDescriptor = null;

            groupDescriptors = _channels.Values
                .Where(state => state.Type == ChatChannelType.Group)
                .Select(state => state.Descriptor)
                .ToList();

            _selfTokens.Clear();
            _pendingSelfMessages.Clear();

            foreach (var state in _channels.Values)
            {
                state.IsConnected = false;
                state.IsAvailable = false;
                state.StatusText = "Chat services disabled";
            }

            MarkChannelsSnapshotDirtyLocked();
        }

        UnregisterChatHandler();

        if (zoneDescriptor.HasValue)
        {
            await SendPresenceAsync(zoneDescriptor.Value, 0, isActive: false, force: true).ConfigureAwait(false);
        }

        foreach (var descriptor in groupDescriptors)
        {
            await SendPresenceAsync(descriptor, 0, isActive: false, force: true).ConfigureAwait(false);
        }

        PublishChannelListChanged();
    }

    public async Task<bool> SendMessageAsync(ChatChannelDescriptor descriptor, string message)
    {
        if (!_chatEnabled)
            return false;

        if (string.IsNullOrWhiteSpace(message))
            return false;

        var sanitized = message.Trim().ReplaceLineEndings(" ");
        if (sanitized.Length == 0)
            return false;

        if (sanitized.Length > MaxOutgoingLength)
            sanitized = sanitized[..MaxOutgoingLength];

        var pendingMessage = EnqueuePendingSelfMessage(descriptor, sanitized);

        try
        {
            await _apiController.SendChatMessage(new ChatSendRequestDto(descriptor, sanitized)).ConfigureAwait(false);
            return true;
        }
        catch (Exception ex)
        {
            RemovePendingSelfMessage(pendingMessage);
            Logger.LogWarning(ex, "Failed to send chat message");
            return false;
        }
    }

    public async Task<bool> SetParticipantMuteAsync(ChatChannelDescriptor descriptor, string token, bool mute)
    {
        if (string.IsNullOrWhiteSpace(token))
            return false;

        try
        {
            await _apiController.SetChatParticipantMute(new ChatParticipantMuteRequestDto(descriptor, token, mute)).ConfigureAwait(false);
            return true;
        }
        catch (Exception ex)
        {
            Logger.LogWarning(ex, "Failed to update chat participant mute state");
            return false;
        }
    }

    public Task<ChatReportResult> ReportMessageAsync(ChatChannelDescriptor descriptor, string messageId, string reason, string? additionalContext)
    {
        if (string.IsNullOrWhiteSpace(messageId))
        {
            return Task.FromResult(new ChatReportResult(false, "Unable to locate the selected message."));
        }

        var trimmedReason = reason?.Trim() ?? string.Empty;
        if (trimmedReason.Length == 0)
        {
            return Task.FromResult(new ChatReportResult(false, "Please describe why you are reporting this message."));
        }

        using (_sync.EnterScope())
        {
            if (!_chatEnabled)
            {
                return Task.FromResult(new ChatReportResult(false, "Enable chat before reporting messages."));
            }

            if (!_isConnected)
            {
                return Task.FromResult(new ChatReportResult(false, "Connect to the chat server before reporting messages."));
            }
        }

        if (trimmedReason.Length > MaxReportReasonLength)
        {
            trimmedReason = trimmedReason[..MaxReportReasonLength];
        }

        string? context = null;
        if (!string.IsNullOrWhiteSpace(additionalContext))
        {
            context = additionalContext.Trim();
            if (context.Length > MaxReportContextLength)
            {
                context = context[..MaxReportContextLength];
            }
        }

        var normalizedDescriptor = descriptor.WithNormalizedCustomKey();
        return ReportMessageInternalAsync(normalizedDescriptor, messageId.Trim(), trimmedReason, context);
    }

    private async Task<ChatReportResult> ReportMessageInternalAsync(ChatChannelDescriptor descriptor, string messageId, string reason, string? additionalContext)
    {
        try
        {
            await _apiController.ReportChatMessage(new ChatReportSubmitDto(descriptor, messageId, reason, additionalContext)).ConfigureAwait(false);
            return new ChatReportResult(true, null);
        }
        catch (Exception ex)
        {
            Logger.LogWarning(ex, "Failed to submit chat report");
            return new ChatReportResult(false, "Failed to submit report. Please try again.");
        }
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        Mediator.Subscribe<DalamudLoginMessage>(this, _ => HandleLogin());
        Mediator.Subscribe<DalamudLogoutMessage>(this, _ => HandleLogout());
        Mediator.Subscribe<ZoneSwitchEndMessage>(this, _ => ScheduleZonePresenceUpdate());
        Mediator.Subscribe<WorldChangedMessage>(this, _ => ScheduleZonePresenceUpdate(force: true));
        Mediator.Subscribe<ConnectedMessage>(this, _ => HandleConnected());
        Mediator.Subscribe<HubReconnectedMessage>(this, _ => HandleConnected());
        Mediator.Subscribe<HubReconnectingMessage>(this, _ => HandleReconnecting());
        Mediator.Subscribe<DisconnectedMessage>(this, _ => HandleReconnecting());
        Mediator.Subscribe<PairUiUpdatedMessage>(this, _ => RefreshGroupsFromPairManager());
        Mediator.Subscribe<BroadcastStatusChangedMessage>(this, _ => ScheduleZonePresenceUpdate(force: true));

        if (_chatEnabled)
        {
            RegisterChatHandler();
            _ = RefreshChatChannelDefinitionsAsync();
            ScheduleZonePresenceUpdate(force: true);
            _ = EnsureGroupPresenceAsync(force: true);
        }
        else
        {
            UpdateChannelsForDisabledState();
            PublishChannelListChanged();
        }

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        UnregisterChatHandler();
        UnsubscribeAll();
        return Task.CompletedTask;
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            UnregisterChatHandler();
            UnsubscribeAll();
        }

        base.Dispose(disposing);
    }

    private void HandleLogin()
    {
        _isLoggedIn = true;
        if (_chatEnabled)
        {
            ScheduleZonePresenceUpdate(force: true);
            _ = EnsureGroupPresenceAsync(force: true);
        }
    }

    private void HandleLogout()
    {
        _isLoggedIn = false;
        if (_chatEnabled)
        {
            ScheduleZonePresenceUpdate(force: true);
        }
    }

    private void HandleConnected()
    {
        _isConnected = true;

        using (_sync.EnterScope())
        {
            _selfTokens.Clear();
            _pendingSelfMessages.Clear();

            foreach (var state in _channels.Values)
            {
                state.IsConnected = _chatEnabled;
                if (_chatEnabled && state.Type == ChatChannelType.Group)
                {
                    state.IsAvailable = true;
                    state.StatusText = null;
                }
                else if (!_chatEnabled)
                {
                    state.IsAvailable = false;
                    state.StatusText = "Chat services disabled";
                }
            }
        }

        PublishChannelListChanged();

        if (_chatEnabled)
        {
            _ = RefreshChatChannelDefinitionsAsync();
            ScheduleZonePresenceUpdate(force: true);
            _ = EnsureGroupPresenceAsync(force: true);
        }
    }

    private void HandleReconnecting()
    {
        _isConnected = false;

        using (_sync.EnterScope())
        {
            _selfTokens.Clear();
            _pendingSelfMessages.Clear();
            foreach (var state in _channels.Values)
            {
                state.IsConnected = false;
                if (_chatEnabled)
                {
                    state.StatusText = "Disconnected from chat server";
                    if (state.Type == ChatChannelType.Group)
                    {
                        state.IsAvailable = false;
                    }
                }
                else
                {
                    state.StatusText = "Chat services disabled";
                    state.IsAvailable = false;
                }
            }
        }

        PublishChannelListChanged();
    }

    private async Task RefreshChatChannelDefinitionsAsync()
    {
        if (!_chatEnabled)
            return;

        try
        {
            var zones = await _apiController.GetZoneChatChannelsAsync().ConfigureAwait(false);
            var groups = await _apiController.GetGroupChatChannelsAsync().ConfigureAwait(false);

            ApplyZoneDefinitions(zones);
            ApplyGroupDefinitions(groups);
        }
        catch (Exception ex)
        {
            Logger.LogDebug(ex, "Failed to refresh chat channel definitions");
        }
    }

    private void RegisterChatHandler()
    {
        if (_chatHandlerRegistered)
            return;

        _apiController.RegisterChatMessageHandler(OnChatMessageReceived);
        _chatHandlerRegistered = true;
    }

    private void UnregisterChatHandler()
    {
        if (!_chatHandlerRegistered)
            return;

        _apiController.UnregisterChatMessageHandler(OnChatMessageReceived);
        _chatHandlerRegistered = false;
    }

    private void UpdateChannelsForDisabledState()
    {
        using (_sync.EnterScope())
        {
            foreach (var state in _channels.Values)
            {
                state.IsConnected = false;
                state.IsAvailable = false;
                state.StatusText = "Chat services disabled";
            }
        }
    }

    private void ScheduleZonePresenceUpdate(bool force = false)
    {
        if (!_chatEnabled)
            return;

        _ = UpdateZonePresenceAsync(force);
    }

    private async Task UpdateZonePresenceAsync(bool force = false)
    {
        if (!_chatEnabled)
            return;

        if (!_isLoggedIn || !_apiController.IsConnected)
        {
            await LeaveCurrentZoneAsync(force, 0, 0).ConfigureAwait(false);
            return;
        }

        try
        {
            var location = _dalamudUtilService.GetMapData();
            var territoryId = (ushort)location.TerritoryId;
            var worldId = (ushort)location.ServerId;

            string? zoneKey;
            ZoneChannelDefinition? definition = null;

            using (_sync.EnterScope())
            {
                _territoryToZoneKey.TryGetValue(territoryId, out zoneKey);
                if (zoneKey is not null)
                {
                    _zoneDefinitions.TryGetValue(zoneKey, out var def);
                    definition = def;
                }
            }

            if (definition is null)
            {
                await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false);
                return;
            }

            var descriptor = await BuildZoneDescriptorAsync(definition.Value).ConfigureAwait(false);
            if (descriptor is null)
            {
                await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false);
                return;
            }

            bool shouldForceSend;
            ChatMessageEntry? zoneSeparatorEntry = null;

            using (_sync.EnterScope())
            {
                var state = EnsureZoneStateLocked();
                state.DisplayName = definition.Value.DisplayName;
                state.Descriptor = descriptor.Value;
                state.IsConnected = _chatEnabled && _isConnected;
                state.IsAvailable = _chatEnabled;
                state.StatusText = _chatEnabled ? null : "Chat services disabled";

                var previousDescriptor = _lastZoneDescriptor;
                var zoneChanged = previousDescriptor.HasValue && !ChannelDescriptorsMatch(previousDescriptor.Value, descriptor.Value);

                _activeChannelKey = ZoneChannelKey;
                shouldForceSend = force || !previousDescriptor.HasValue || zoneChanged;
                if (zoneChanged && state.Messages.Any(m => !m.IsSystem))
                {
                    zoneSeparatorEntry = AddZoneSeparatorLocked(state, definition.Value.DisplayName);
                }

                _lastZoneDescriptor = descriptor;
            }

            if (zoneSeparatorEntry is not null)
            {
                Mediator.Publish(new ChatChannelMessageAdded(ZoneChannelKey, zoneSeparatorEntry));
            }

            PublishChannelListChanged();
            await SendPresenceAsync(descriptor.Value, territoryId, isActive: true, force: shouldForceSend).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Logger.LogDebug(ex, "Failed to update zone chat presence");
        }
    }

    private async Task LeaveCurrentZoneAsync(bool force, ushort territoryId, ushort worldId)
    {
        ChatChannelDescriptor? descriptor = null;

        using (_sync.EnterScope())
        {
            descriptor = _lastZoneDescriptor;
            _lastZoneDescriptor = null;

            if (_channels.TryGetValue(ZoneChannelKey, out var state))
            {
                state.IsConnected = _isConnected;
                state.IsAvailable = false;
                state.StatusText = !_chatEnabled
                    ? "Chat services disabled"
                    : (_isConnected ? ZoneUnavailableMessage : "Disconnected from chat server");
                if (territoryId != 0
                    && _dalamudUtilService.TerritoryData.Value.TryGetValue(territoryId, out var territoryName)
                    && !string.IsNullOrWhiteSpace(territoryName))
                {
                    state.DisplayName = territoryName;
                }
                else
                {
                    state.DisplayName = "Zone Chat";
                }

                if (worldId != 0)
                {
                    state.Descriptor = new ChatChannelDescriptor
                    {
                        Type = ChatChannelType.Zone,
                        WorldId = worldId,
                        ZoneId = territoryId,
                        CustomKey = string.Empty
                    };
                }
            }

            if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal))
            {
                _activeChannelKey = _channelOrder.FirstOrDefault(key => !string.Equals(key, ZoneChannelKey, StringComparison.Ordinal));
            }
        }

        PublishChannelListChanged();

        if (descriptor.HasValue)
        {
            await SendPresenceAsync(descriptor.Value, territoryId, isActive: false, force: force).ConfigureAwait(false);
        }
    }

    private async Task<ChatChannelDescriptor?> BuildZoneDescriptorAsync(ZoneChannelDefinition definition)
    {
        try
        {
            var worldId = (ushort)_dalamudUtilService.GetWorldId();
            return definition.Descriptor with { WorldId = worldId };
        }
        catch (Exception ex)
        {
            Logger.LogDebug(ex, "Failed to obtain world id for zone chat");
            return null;
        }
    }

    private void ApplyZoneDefinitions(IReadOnlyList<ZoneChatChannelInfoDto>? infos)
    {
        var infoList = infos ?? Array.Empty<ZoneChatChannelInfoDto>();

        using (_sync.EnterScope())
        {
            _zoneDefinitions.Clear();
            _territoryToZoneKey.Clear();

            foreach (var info in infoList)
            {
                var descriptor = info.Channel.WithNormalizedCustomKey();
                var key = descriptor.CustomKey ?? string.Empty;
                if (string.IsNullOrWhiteSpace(key))
                    continue;

                var territories = info.Territories?
                    .SelectMany(EnumerateTerritoryKeys)
                    .Where(n => n.Length > 0)
                    .ToHashSet(StringComparer.OrdinalIgnoreCase)
                    ?? new HashSet<string>(StringComparer.OrdinalIgnoreCase);

                _zoneDefinitions[key] = new ZoneChannelDefinition(key, info.DisplayName ?? key, descriptor, territories);
            }

            var territoryData = _dalamudUtilService.TerritoryDataEnglish.Value;
            foreach (var kvp in territoryData)
            {
                foreach (var variant in EnumerateTerritoryKeys(kvp.Value))
                {
                    foreach (var def in _zoneDefinitions.Values)
                    {
                        if (def.TerritoryNames.Contains(variant))
                        {
                            _territoryToZoneKey[kvp.Key] = def.Key;
                            break;
                        }
                    }
                }
            }

            if (_zoneDefinitions.Count == 0)
            {
                RemoveZoneStateLocked();
            }
            else
            {
                var state = EnsureZoneStateLocked();
                state.DisplayName = "Zone Chat";
                state.IsConnected = _chatEnabled && _isConnected;
                state.IsAvailable = false;
                state.StatusText = _chatEnabled ? ZoneUnavailableMessage : "Chat services disabled";
            }

            UpdateChannelOrderLocked();
        }

        PublishChannelListChanged();
    }

    private void ApplyGroupDefinitions(IReadOnlyList<GroupChatChannelInfoDto>? infos)
    {
        var infoList = infos ?? Array.Empty<GroupChatChannelInfoDto>();
        var descriptorsToJoin = new List<ChatChannelDescriptor>();
        var descriptorsToLeave = new List<ChatChannelDescriptor>();

        using (_sync.EnterScope())
        {
            var remainingGroups = new HashSet<string>(_groupDefinitions.Keys, StringComparer.OrdinalIgnoreCase);
            var allowRemoval = _isConnected;

            foreach (var info in infoList)
            {
                var descriptor = info.Channel.WithNormalizedCustomKey();
                var groupId = info.GroupId;
                if (string.IsNullOrWhiteSpace(groupId))
                    continue;

                remainingGroups.Remove(groupId);

                _groupDefinitions[groupId] = new GroupChannelDefinition(groupId, info.DisplayName ?? groupId, descriptor, info.IsOwner);

                var key = BuildChannelKey(descriptor);
                if (!_channels.TryGetValue(key, out var state))
                {
                state = new ChatChannelState(key, ChatChannelType.Group, info.DisplayName ?? groupId, descriptor);
                var restoredCount = RestoreCachedMessagesLocked(state);
                state.IsConnected = _chatEnabled && _isConnected;
                state.IsAvailable = _chatEnabled && _isConnected;
                state.StatusText = !_chatEnabled
                    ? "Chat services disabled"
                    : (_isConnected ? null : "Disconnected from chat server");
                _channels[key] = state;
                _lastReadCounts[key] = restoredCount > 0 ? state.Messages.Count : 0;
                if (_chatEnabled)
                {
                    descriptorsToJoin.Add(descriptor);
                }
                }
                else
                {
                    state.DisplayName = info.DisplayName ?? groupId;
                    state.Descriptor = descriptor;
                    state.IsConnected = _chatEnabled && _isConnected;
                    state.IsAvailable = _chatEnabled && _isConnected;
                    state.StatusText = !_chatEnabled
                        ? "Chat services disabled"
                        : (_isConnected ? null : "Disconnected from chat server");
                }
            }

            if (allowRemoval)
            {
                foreach (var removedGroupId in remainingGroups)
                {
                    if (_groupDefinitions.TryGetValue(removedGroupId, out var definition))
                    {
                        var key = BuildChannelKey(definition.Descriptor);
                        if (_channels.TryGetValue(key, out var state))
                        {
                            CacheMessagesLocked(state);
                            descriptorsToLeave.Add(state.Descriptor);
                            _channels.Remove(key);
                            _lastReadCounts.Remove(key);
                            _lastPresenceStates.Remove(BuildPresenceKey(state.Descriptor));
                            _selfTokens.Remove(key);
                            _pendingSelfMessages.RemoveAll(p => string.Equals(p.ChannelKey, key, StringComparison.Ordinal));
                            if (string.Equals(_activeChannelKey, key, StringComparison.Ordinal))
                            {
                                _activeChannelKey = null;
                            }
                        }

                        _groupDefinitions.Remove(removedGroupId);
                    }
                }
            }

            UpdateChannelOrderLocked();
        }

        foreach (var descriptor in descriptorsToLeave)
        {
            _ = SendPresenceAsync(descriptor, 0, isActive: false, force: true);
        }

        foreach (var descriptor in descriptorsToJoin)
        {
            _ = SendPresenceAsync(descriptor, 0, isActive: true, force: true);
        }

        PublishChannelListChanged();
    }

    private void RefreshGroupsFromPairManager()
    {
        var snapshot = _pairUiService.GetSnapshot();
        var groups = snapshot.Groups.ToList();
        if (groups.Count == 0)
        {
            ApplyGroupDefinitions(Array.Empty<GroupChatChannelInfoDto>());
            return;
        }

        var infos = new List<GroupChatChannelInfoDto>(groups.Count);
        foreach (var group in groups)
        {
            // basically prune the channel if it's disabled
            if (group.GroupPermissions.IsDisableChat())
            {
                continue;
            }

            var descriptor = new ChatChannelDescriptor
            {
                Type = ChatChannelType.Group,
                WorldId = 0,
                ZoneId = 0,
                CustomKey = group.Group.GID
            };

            var displayName = string.IsNullOrWhiteSpace(group.Group.Alias) ? group.Group.GID : group.Group.Alias;
            var isOwner = string.Equals(group.Owner.UID, _apiController.UID, StringComparison.Ordinal);

            infos.Add(new GroupChatChannelInfoDto(descriptor, displayName, group.Group.GID, isOwner));
        }

        ApplyGroupDefinitions(infos);
    }

    private async Task EnsureGroupPresenceAsync(bool force = false)
    {
        if (!_chatEnabled)
            return;

        List<ChatChannelDescriptor> descriptors;
        using (_sync.EnterScope())
        {
            descriptors = _channels.Values
                .Where(state => state.Type == ChatChannelType.Group)
                .Select(state => state.Descriptor)
                .ToList();
        }

        foreach (var descriptor in descriptors)
        {
            await SendPresenceAsync(descriptor, 0, isActive: true, force: force).ConfigureAwait(false);
        }
    }

    private async Task SendPresenceAsync(ChatChannelDescriptor descriptor, ushort territoryId, bool isActive, bool force)
    {
        if (!_apiController.IsConnected)
            return;

        if (!_chatEnabled && isActive)
            return;

        var presenceKey = BuildPresenceKey(descriptor);
        bool stateMatches;

        using (_sync.EnterScope())
        {
            stateMatches = !force
                           && _lastPresenceStates.TryGetValue(presenceKey, out var lastState)
                           && lastState == isActive;
        }

        if (stateMatches)
            return;

        try
        {
            await _apiController.UpdateChatPresence(new ChatPresenceUpdateDto(descriptor, territoryId, isActive)).ConfigureAwait(false);

            using (_sync.EnterScope())
            {
                if (isActive)
                {
                    _lastPresenceStates[presenceKey] = true;
                }
                else
                {
                    _lastPresenceStates.Remove(presenceKey);
                }
            }
        }
        catch (Exception ex)
        {
            Logger.LogDebug(ex, "Failed to update chat presence");
        }
    }

    private PendingSelfMessage EnqueuePendingSelfMessage(ChatChannelDescriptor descriptor, string message)
    {
        var normalized = descriptor.WithNormalizedCustomKey();
        var key = normalized.Type == ChatChannelType.Zone ? ZoneChannelKey : BuildChannelKey(normalized);
        var pending = new PendingSelfMessage(key, message);

        using (_sync.EnterScope())
        {
            _pendingSelfMessages.Add(pending);
            while (_pendingSelfMessages.Count > 20)
            {
                _pendingSelfMessages.RemoveAt(0);
            }
        }

        return pending;
    }

    private void RemovePendingSelfMessage(PendingSelfMessage pending)
    {
        using (_sync.EnterScope())
        {
            var index = _pendingSelfMessages.FindIndex(p =>
                string.Equals(p.ChannelKey, pending.ChannelKey, StringComparison.Ordinal) &&
                string.Equals(p.Message, pending.Message, StringComparison.Ordinal));

            if (index >= 0)
            {
                _pendingSelfMessages.RemoveAt(index);
            }
        }
    }

    private void OnChatMessageReceived(ChatMessageDto dto)
    {
        var descriptor = dto.Channel.WithNormalizedCustomKey();
        var key = descriptor.Type == ChatChannelType.Zone ? ZoneChannelKey : BuildChannelKey(descriptor);
        var fromSelf = IsMessageFromSelf(dto, key);
        var message = BuildMessage(dto, fromSelf);
        bool publishChannelList = false;

        using (_sync.EnterScope())
        {
            if (!_channels.TryGetValue(key, out var state))
            {
                var displayName = descriptor.Type switch
                {
                    ChatChannelType.Zone => _zoneDefinitions.TryGetValue(descriptor.CustomKey ?? string.Empty, out var def)
                        ? def.DisplayName
                        : "Zone Chat",
                    ChatChannelType.Group => descriptor.CustomKey ?? "Syncshell",
                    _ => descriptor.CustomKey ?? "Chat"
                };

                state = new ChatChannelState(
                    key,
                    descriptor.Type,
                    displayName,
                    descriptor.Type == ChatChannelType.Zone ? (_lastZoneDescriptor ?? descriptor) : descriptor);
                var restoredCount = RestoreCachedMessagesLocked(state);

                state.IsConnected = _isConnected;
                state.IsAvailable = descriptor.Type == ChatChannelType.Group && _isConnected;
                state.StatusText = descriptor.Type == ChatChannelType.Zone ? ZoneUnavailableMessage : (_isConnected ? null : "Disconnected from chat server");

                _channels[key] = state;
                _lastReadCounts[key] = restoredCount > 0 ? state.Messages.Count : 0;
                publishChannelList = true;
            }

            state.Descriptor = descriptor.Type == ChatChannelType.Zone ? (_lastZoneDescriptor ?? descriptor) : descriptor;
            state.Messages.Add(message);
            if (state.Messages.Count > MaxMessageHistory)
            {
                state.Messages.RemoveAt(0);
            }

            if (string.Equals(_activeChannelKey, key, StringComparison.Ordinal))
            {
                state.HasUnread = false;
                state.UnreadCount = 0;
                _lastReadCounts[key] = state.Messages.Count;
            }
            else
            {
                var lastRead = _lastReadCounts.TryGetValue(key, out var readCount) ? readCount : 0;
                var unreadFromHistory = Math.Max(0, state.Messages.Count - lastRead);
                var incrementalUnread = Math.Min(state.UnreadCount + 1, MaxUnreadCount);
                state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount);
                state.HasUnread = state.UnreadCount > 0;
            }

            MarkChannelsSnapshotDirtyLocked();
        }

        Mediator.Publish(new ChatChannelMessageAdded(key, message));

        if (publishChannelList)
        {
            using (_sync.EnterScope())
            {
                UpdateChannelOrderLocked();
            }

            PublishChannelListChanged();
        }
    }

    private bool IsMessageFromSelf(ChatMessageDto dto, string channelKey)
    {
        if (dto.Sender.User?.UID is { } uid && string.Equals(uid, _apiController.UID, StringComparison.Ordinal))
        {
            using (_sync.EnterScope())
            {
                _selfTokens[channelKey] = dto.Sender.Token;
            }

            return true;
        }

        using (_sync.EnterScope())
        {
            if (_selfTokens.TryGetValue(channelKey, out var token) &&
                string.Equals(token, dto.Sender.Token, StringComparison.Ordinal))
            {
                return true;
            }

            var index = _pendingSelfMessages.FindIndex(p =>
                string.Equals(p.ChannelKey, channelKey, StringComparison.Ordinal) &&
                string.Equals(p.Message, dto.Message, StringComparison.Ordinal));

            if (index >= 0)
            {
                _pendingSelfMessages.RemoveAt(index);
                _selfTokens[channelKey] = dto.Sender.Token;
                return true;
            }
        }

        return false;
    }

    private ChatMessageEntry BuildMessage(ChatMessageDto dto, bool fromSelf)
    {
        var displayName = ResolveDisplayName(dto, fromSelf);
        return new ChatMessageEntry(dto, displayName, fromSelf, DateTime.UtcNow);
    }

    private ChatMessageEntry AddZoneSeparatorLocked(ChatChannelState state, string zoneDisplayName)
    {
        var separator = new ChatMessageEntry(
            null,
            string.Empty,
            false,
            DateTime.UtcNow,
            new ChatSystemEntry(ChatSystemEntryType.ZoneSeparator, zoneDisplayName));

        state.Messages.Add(separator);
        if (state.Messages.Count > MaxMessageHistory)
        {
            state.Messages.RemoveAt(0);
        }

        if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal))
        {
            state.HasUnread = false;
            state.UnreadCount = 0;
            _lastReadCounts[ZoneChannelKey] = state.Messages.Count;
        }
        else if (_lastReadCounts.TryGetValue(ZoneChannelKey, out var readCount))
        {
            _lastReadCounts[ZoneChannelKey] = readCount + 1;
        }
        else
        {
            _lastReadCounts[ZoneChannelKey] = state.Messages.Count;
        }

        return separator;
    }

    private string ResolveDisplayName(ChatMessageDto dto, bool fromSelf)
    {
        var isZone = dto.Channel.Type == ChatChannelType.Zone;
        if (!string.IsNullOrEmpty(dto.Sender.HashedCid) &&
            _actorObjectService.TryGetValidatedActorByHash(dto.Sender.HashedCid, out var descriptor) &&
            !string.IsNullOrWhiteSpace(descriptor.Name))
        {
            return descriptor.Name;
        }

        if (fromSelf && isZone && dto.Sender.CanResolveProfile)
        {
            try
            {
                return _dalamudUtilService.GetPlayerName();
            }
            catch (Exception ex)
            {
                Logger.LogDebug(ex, "Failed to resolve self name for chat message");
            }
        }

        if (dto.Sender.Kind == ChatSenderKind.IdentifiedUser && dto.Sender.User is not null)
        {
            if (dto.Channel.Type != ChatChannelType.Group || _chatConfigService.Current.ShowNotesInSyncshellChat)
            {
                var note = _serverConfigurationManager.GetNoteForUid(dto.Sender.User.UID);
                if (!string.IsNullOrWhiteSpace(note))
                {
                    return note;
                }
            }

            return dto.Sender.User.AliasOrUID;
        }

        if (!string.IsNullOrWhiteSpace(dto.Sender.DisplayName))
        {
            return dto.Sender.DisplayName!;
        }

        return dto.Sender.Token;
    }

    private void UpdateChannelOrderLocked()
    {
        _channelOrder.Clear();

        var configuredOrder = _chatConfigService.Current.ChannelOrder;
        if (configuredOrder.Count > 0)
        {
            var seen = new HashSet<string>(StringComparer.Ordinal);
            foreach (var key in configuredOrder)
            {
                if (_channels.ContainsKey(key) && seen.Add(key))
                {
                    _channelOrder.Add(key);
                }
            }

            var remaining = _channels.Values
                .Where(state => !seen.Contains(state.Key))
                .ToList();

            if (remaining.Count > 0)
            {
                var zoneKeys = remaining
                    .Where(state => state.Type == ChatChannelType.Zone)
                    .Select(state => state.Key);
                var groupKeys = remaining
                    .Where(state => state.Type == ChatChannelType.Group)
                    .OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase)
                    .Select(state => state.Key);

                _channelOrder.AddRange(zoneKeys);
                _channelOrder.AddRange(groupKeys);
            }
        }
        else
        {
            if (_channels.ContainsKey(ZoneChannelKey))
            {
                _channelOrder.Add(ZoneChannelKey);
            }

            var groups = _channels.Values
                .Where(state => state.Type == ChatChannelType.Group)
                .OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase)
                .Select(state => state.Key);

            _channelOrder.AddRange(groups);
        }

        if (_activeChannelKey is null && _channelOrder.Count > 0)
        {
            _activeChannelKey = _channelOrder[0];
        }
        else if (_activeChannelKey is not null && !_channelOrder.Contains(_activeChannelKey, StringComparer.Ordinal))
        {
            _activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null;
        }

        MarkChannelsSnapshotDirtyLocked();
    }

    private void MarkChannelsSnapshotDirty()
    {
        using (_sync.EnterScope())
        {
            _channelsSnapshotDirty = true;
        }
    }

    private void MarkChannelsSnapshotDirtyLocked() => _channelsSnapshotDirty = true;

    private void PublishChannelListChanged()
    {
        MarkChannelsSnapshotDirty();
        Mediator.Publish(new ChatChannelsUpdated());
    }

    private static IEnumerable<string> EnumerateTerritoryKeys(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
            yield break;

        var normalizedFull = NormalizeKey(value);
        if (normalizedFull.Length > 0)
            yield return normalizedFull;

        var segments = value.Split('-', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
        if (segments.Length <= 1)
            yield break;

        for (var i = 1; i < segments.Length; i++)
        {
            var composite = string.Join(" - ", segments[i..]);
            var normalized = NormalizeKey(composite);
            if (normalized.Length > 0)
                yield return normalized;
        }
    }

    private static string NormalizeKey(string? value)
        => string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim().ToUpperInvariant();

    private static string BuildChannelKey(ChatChannelDescriptor descriptor)
        => $"{(int)descriptor.Type}:{NormalizeKey(descriptor.CustomKey)}";

    private static string BuildPresenceKey(ChatChannelDescriptor descriptor)
        => $"{(int)descriptor.Type}:{descriptor.WorldId}:{NormalizeKey(descriptor.CustomKey)}";

    private static bool ChannelDescriptorsMatch(ChatChannelDescriptor left, ChatChannelDescriptor right)
        => left.Type == right.Type
           && string.Equals(NormalizeKey(left.CustomKey), NormalizeKey(right.CustomKey), StringComparison.Ordinal)
           && left.WorldId == right.WorldId;

    private ChatChannelState EnsureZoneStateLocked()
    {
        if (!_channels.TryGetValue(ZoneChannelKey, out var state))
        {
            state = new ChatChannelState(ZoneChannelKey, ChatChannelType.Zone, "Zone Chat", new ChatChannelDescriptor { Type = ChatChannelType.Zone });
            var restoredCount = RestoreCachedMessagesLocked(state);
            state.IsConnected = _chatEnabled && _isConnected;
            state.IsAvailable = false;
            state.StatusText = _chatEnabled ? ZoneUnavailableMessage : "Chat services disabled";
            _channels[ZoneChannelKey] = state;
            _lastReadCounts[ZoneChannelKey] = restoredCount > 0 ? state.Messages.Count : 0;
            UpdateChannelOrderLocked();
        }

        return state;
    }

    private void RemoveZoneStateLocked()
    {
        if (_channels.TryGetValue(ZoneChannelKey, out var existing))
        {
            CacheMessagesLocked(existing);
        }

        if (_channels.Remove(ZoneChannelKey))
        {
            _lastReadCounts.Remove(ZoneChannelKey);
            _lastPresenceStates.Remove(BuildPresenceKey(new ChatChannelDescriptor { Type = ChatChannelType.Zone }));
            _selfTokens.Remove(ZoneChannelKey);
            _pendingSelfMessages.RemoveAll(p => string.Equals(p.ChannelKey, ZoneChannelKey, StringComparison.Ordinal));
            if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal))
            {
                _activeChannelKey = null;
            }
            UpdateChannelOrderLocked();
        }
    }

    private void CacheMessagesLocked(ChatChannelState state)
    {
        if (state.Messages.Count == 0)
        {
            return;
        }

        _messageHistoryCache[state.Key] = new List<ChatMessageEntry>(state.Messages);
    }

    private int RestoreCachedMessagesLocked(ChatChannelState state)
    {
        if (_messageHistoryCache.TryGetValue(state.Key, out var cached) && cached.Count > 0)
        {
            state.Messages.AddRange(cached);
            _messageHistoryCache.Remove(state.Key);
            return cached.Count;
        }

        return 0;
    }

    private sealed class ChatChannelState
    {
        public ChatChannelState(string key, ChatChannelType type, string displayName, ChatChannelDescriptor descriptor)
        {
            Key = key;
            Type = type;
            DisplayName = displayName;
            Descriptor = descriptor;
            Messages = new List<ChatMessageEntry>();
        }

        public string Key { get; }
        public ChatChannelType Type { get; }
        public string DisplayName { get; set; }
        public ChatChannelDescriptor Descriptor { get; set; }
        public bool IsConnected { get; set; }
        public bool IsAvailable { get; set; }
        public string? StatusText { get; set; }
        public bool HasUnread { get; set; }
        public int UnreadCount { get; set; }
        public List<ChatMessageEntry> Messages { get; }
    }

    private readonly record struct ZoneChannelDefinition(
        string Key,
        string DisplayName,
        ChatChannelDescriptor Descriptor,
        HashSet<string> TerritoryNames);

    private readonly record struct GroupChannelDefinition(
        string GroupId,
        string DisplayName,
        ChatChannelDescriptor Descriptor,
        bool IsOwner);

    private readonly record struct PendingSelfMessage(string ChannelKey, string Message);
}
