using System.Diagnostics.CodeAnalysis;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User;

namespace LightlessSync.PlayerData.Pairs;

/// <summary>
///     in memory state for pairs, groups, and syncshells
/// </summary>
public sealed class PairManager
{
    private readonly object _gate = new();
    private readonly Dictionary<string, PairConnection> _pairs = new(StringComparer.Ordinal);
    private readonly Dictionary<string, Syncshell> _groups = new(StringComparer.Ordinal);

    public PairConnection? LastAddedUser { get; private set; }

    public IReadOnlyDictionary<string, PairConnection> GetAllPairs()
    {
        lock (_gate)
        {
            return new Dictionary<string, PairConnection>(_pairs);
        }
    }

    public IReadOnlyDictionary<string, Syncshell> GetAllGroups()
    {
        lock (_gate)
        {
            return new Dictionary<string, Syncshell>(_groups);
        }
    }

    public PairConnection? GetLastAddedUser()
    {
        lock (_gate)
        {
            return LastAddedUser;
        }
    }

    public void ClearLastAddedUser()
    {
        lock (_gate)
        {
            LastAddedUser = null;
        }
    }

    public void ClearAll()
    {
        lock (_gate)
        {
            _pairs.Clear();
            _groups.Clear();
            LastAddedUser = null;
        }
    }

    public PairOperationResult<PairConnection> GetPair(string userId)
    {
        lock (_gate)
        {
            if (_pairs.TryGetValue(userId, out var connection))
            {
                return PairOperationResult<PairConnection>.Ok(connection);
            }

            return PairOperationResult<PairConnection>.Fail($"Pair {userId} not found.");
        }
    }

    public bool TryGetPair(string userId, [NotNullWhen(true)] out PairConnection? connection)
    {
        lock (_gate)
        {
            return _pairs.TryGetValue(userId, out connection);
        }
    }

    public PairOperationResult<Syncshell> GetGroup(string groupId)
    {
        lock (_gate)
        {
            if (_groups.TryGetValue(groupId, out var shell))
            {
                return PairOperationResult<Syncshell>.Ok(shell);
            }

            return PairOperationResult<Syncshell>.Fail($"Group {groupId} not found.");
        }
    }

    public IReadOnlyList<PairConnection> GetDirectPairs()
    {
        lock (_gate)
        {
            return _pairs.Values.Where(p => p.IsDirectlyPaired).ToList();
        }
    }

    public IReadOnlyList<PairConnection> GetPairsByIdent(string ident)
    {
        lock (_gate)
        {
            return _pairs.Values
                .Where(p => p.Ident is not null && string.Equals(p.Ident, ident, StringComparison.Ordinal))
                .ToList();
        }
    }

    public IReadOnlyList<Syncshell> GetOwnedOrModeratedShells(string currentUserUid)
    {
        lock (_gate)
        {
            return _groups.Values
                .Where(s =>
                    string.Equals(s.GroupFullInfo.Owner.UID, currentUserUid, StringComparison.OrdinalIgnoreCase)
                    || s.GroupFullInfo.GroupUserInfo.HasFlag(GroupPairUserInfo.IsModerator))
                .ToList();
        }
    }

    public PairOperationResult<UserPermissions> GetPairCombinedPermissions(string userId)
    {
        lock (_gate)
        {
            if (!_pairs.TryGetValue(userId, out var connection))
            {
                return PairOperationResult<UserPermissions>.Fail($"Pair {userId} not found.");
            }

            var combined = connection.SelfToOtherPermissions | connection.OtherToSelfPermissions;
            return PairOperationResult<UserPermissions>.Ok(combined);
        }
    }

    public PairOperationResult<PairRegistration> MarkOnline(OnlineUserIdentDto dto)
    {
        lock (_gate)
        {
            if (!_pairs.TryGetValue(dto.User.UID, out var connection))
            {
                connection = GetOrCreatePair(dto.User);
            }

            connection.SetOnline(dto.Ident);
            return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), dto.Ident));
        }
    }

    public PairOperationResult<PairRegistration> MarkOffline(UserData user)
    {
        lock (_gate)
        {
            if (!_pairs.TryGetValue(user.UID, out var connection))
            {
                return PairOperationResult<PairRegistration>.Fail($"Pair {user.UID} not found.");
            }

            connection.SetOffline();
            return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(user.UID), connection.Ident));
        }
    }

    public PairOperationResult<PairRegistration> AddOrUpdateIndividual(UserPairDto dto, bool markAsLastAddedUser = true)
    {
        lock (_gate)
        {
            var connection = GetOrCreatePair(dto.User, out var created);
            connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions);
            connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus);

            if (connection.Ident is null)
            {
                return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), null));
            }

            if (created && markAsLastAddedUser)
            {
                LastAddedUser = connection;
            }

            return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident));
        }
    }

    public PairOperationResult<PairRegistration> AddOrUpdateIndividual(UserFullPairDto dto)
    {
        lock (_gate)
        {
            var connection = GetOrCreatePair(dto.User, out _);
            connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions);
            connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus);

            var removedGroups = connection.Groups.Keys.Where(k => !dto.Groups.Contains(k, StringComparer.Ordinal)).ToList();
            foreach (var groupId in removedGroups)
            {
                connection.RemoveGroupRelationship(groupId);
                if (_groups.TryGetValue(groupId, out var shell))
                {
                    shell.Users.Remove(dto.User.UID);
                }
            }

            foreach (var groupId in dto.Groups)
            {
                connection.EnsureGroupRelationship(groupId, null);
                if (_groups.TryGetValue(groupId, out var shell))
                {
                    shell.Users[dto.User.UID] = connection;
                }
            }

            return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident));
        }
    }

    public PairOperationResult<PairRegistration?> RemoveIndividual(UserDto dto)
    {
        lock (_gate)
        {
            if (!_pairs.TryGetValue(dto.User.UID, out var connection))
            {
                return PairOperationResult<PairRegistration?>.Fail($"Pair {dto.User.UID} not found.");
            }

            connection.UpdateStatus(null);
            var registration = TryRemovePairIfNoConnection(connection);
            return PairOperationResult<PairRegistration?>.Ok(registration);
        }
    }

    public PairOperationResult<PairRegistration> SetPairOtherToSelfPermissions(UserPermissionsDto dto)
    {
        lock (_gate)
        {
            if (!_pairs.TryGetValue(dto.User.UID, out var connection))
            {
                return PairOperationResult<PairRegistration>.Fail($"Pair {dto.User.UID} not found.");
            }

            connection.UpdatePermissions(connection.SelfToOtherPermissions, dto.Permissions);
            return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident));
        }
    }

    public PairOperationResult<PairRegistration> SetPairSelfToOtherPermissions(UserPermissionsDto dto)
    {
        lock (_gate)
        {
            if (!_pairs.TryGetValue(dto.User.UID, out var connection))
            {
                return PairOperationResult<PairRegistration>.Fail($"Pair {dto.User.UID} not found.");
            }

            connection.UpdatePermissions(dto.Permissions, connection.OtherToSelfPermissions);
            return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident));
        }
    }

    public PairOperationResult SetIndividualStatus(UserIndividualPairStatusDto dto)
    {
        lock (_gate)
        {
            if (!_pairs.TryGetValue(dto.User.UID, out var connection))
            {
                return PairOperationResult.Fail($"Pair {dto.User.UID} not found.");
            }

            connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus);
            _ = TryRemovePairIfNoConnection(connection);
            return PairOperationResult.Ok();
        }
    }

    public PairOperationResult AddOrUpdateGroupPair(GroupPairFullInfoDto dto)
    {
        lock (_gate)
        {
            var shell = GetOrCreateShell(dto.Group);
            var connection = GetOrCreatePair(dto.User);

            var groupInfo = shell.GroupFullInfo.GroupPairUserInfos.GetValueOrDefault(dto.User.UID, GroupPairUserInfo.None);
            connection.EnsureGroupRelationship(dto.Group.GID, groupInfo == GroupPairUserInfo.None ? null : groupInfo);
            connection.UpdatePermissions(dto.SelfToOtherPermissions, dto.OtherToSelfPermissions);

            shell.Users[dto.User.UID] = connection;
            return PairOperationResult.Ok();
        }
    }

    public PairOperationResult<PairRegistration?> RemoveGroupPair(GroupPairDto dto)
    {
        lock (_gate)
        {
            if (_groups.TryGetValue(dto.GID, out var shell))
            {
                shell.Users.Remove(dto.User.UID);
            }

            PairRegistration? registration = null;
            if (_pairs.TryGetValue(dto.User.UID, out var connection))
            {
                connection.RemoveGroupRelationship(dto.GID);
                registration = TryRemovePairIfNoConnection(connection);
            }

            return PairOperationResult<PairRegistration?>.Ok(registration);
        }
    }

    public PairOperationResult<IReadOnlyList<PairRegistration>> RemoveGroup(string groupId)
    {
        lock (_gate)
        {
            if (!_groups.Remove(groupId, out var shell))
            {
                return PairOperationResult<IReadOnlyList<PairRegistration>>.Fail($"Group {groupId} not found.");
            }

            var removed = new List<PairRegistration>();
            foreach (var connection in shell.Users.Values.ToList())
            {
                connection.RemoveGroupRelationship(groupId);
                var registration = TryRemovePairIfNoConnection(connection);
                if (registration is not null)
                {
                    removed.Add(registration);
                }
            }

            return PairOperationResult<IReadOnlyList<PairRegistration>>.Ok(removed);
        }
    }

    public PairOperationResult AddGroup(GroupFullInfoDto dto)
    {
        lock (_gate)
        {
            if (!_groups.TryGetValue(dto.Group.GID, out var shell))
            {
                shell = new Syncshell(dto);
                _groups[dto.Group.GID] = shell;
            }
            else
            {
                shell.Update(dto);
                shell.Users.Clear();
            }

            foreach (var (userId, info) in dto.GroupPairUserInfos)
            {
                if (_pairs.TryGetValue(userId, out var connection))
                {
                    connection.EnsureGroupRelationship(dto.Group.GID, info == GroupPairUserInfo.None ? null : info);
                    shell.Users[userId] = connection;
                }
            }

            return PairOperationResult.Ok();
        }
    }

    public PairOperationResult UpdateGroupInfo(GroupInfoDto dto)
    {
        lock (_gate)
        {
            if (!_groups.TryGetValue(dto.Group.GID, out var shell))
            {
                return PairOperationResult.Fail($"Group {dto.Group.GID} not found.");
            }

            var updated = new GroupFullInfoDto(
                dto.Group,
                dto.Owner,
                dto.GroupPermissions,
                shell.GroupFullInfo.GroupUserPermissions,
                shell.GroupFullInfo.GroupUserInfo,
                new Dictionary<string, GroupPairUserInfo>(shell.GroupFullInfo.GroupPairUserInfos, StringComparer.Ordinal),
                0);

            shell.Update(updated);
            return PairOperationResult.Ok();
        }
    }

    public PairOperationResult UpdateGroupPairPermissions(GroupPairUserPermissionDto dto)
    {
        lock (_gate)
        {
            if (!_groups.TryGetValue(dto.Group.GID, out var shell))
            {
                return PairOperationResult.Fail($"Group {dto.Group.GID} not found.");
            }

            var updated = shell.GroupFullInfo with { GroupUserPermissions = dto.GroupPairPermissions };
            shell.Update(updated);
            return PairOperationResult.Ok();
        }
    }

    public PairOperationResult UpdateGroupPermissions(GroupPermissionDto dto)
    {
        lock (_gate)
        {
            if (!_groups.TryGetValue(dto.Group.GID, out var shell))
            {
                return PairOperationResult.Fail($"Group {dto.Group.GID} not found.");
            }

            var updated = shell.GroupFullInfo with { GroupPermissions = dto.Permissions };
            shell.Update(updated);
            return PairOperationResult.Ok();
        }
    }

    public PairOperationResult UpdateGroupPairStatus(GroupPairUserInfoDto dto)
    {
        lock (_gate)
        {
            if (_pairs.TryGetValue(dto.UID, out var connection))
            {
                connection.EnsureGroupRelationship(dto.GID, dto.GroupUserInfo == GroupPairUserInfo.None ? null : dto.GroupUserInfo);
            }

            if (_groups.TryGetValue(dto.GID, out var shell))
            {
                var infos = new Dictionary<string, GroupPairUserInfo>(shell.GroupFullInfo.GroupPairUserInfos, StringComparer.Ordinal)
                {
                    [dto.UID] = dto.GroupUserInfo
                };
                var updated = shell.GroupFullInfo with { GroupPairUserInfos = infos };
                shell.Update(updated);
            }

            return PairOperationResult.Ok();
        }
    }

    public PairOperationResult UpdateGroupStatus(GroupPairUserInfoDto dto)
    {
        lock (_gate)
        {
            if (!_groups.TryGetValue(dto.GID, out var shell))
            {
                return PairOperationResult.Fail($"Group {dto.GID} not found.");
            }

            var updated = shell.GroupFullInfo with { GroupUserInfo = dto.GroupUserInfo };
            shell.Update(updated);
            return PairOperationResult.Ok();
        }
    }

    public PairOperationResult UpdateOtherPermissions(UserPermissionsDto dto)
    {
        lock (_gate)
        {
            if (!_pairs.TryGetValue(dto.User.UID, out var connection))
            {
                return PairOperationResult.Fail($"Pair {dto.User.UID} not found.");
            }

            connection.UpdatePermissions(connection.SelfToOtherPermissions, dto.Permissions);
            return PairOperationResult.Ok();
        }
    }

    public PairOperationResult UpdateSelfPermissions(UserPermissionsDto dto)
    {
        lock (_gate)
        {
            if (!_pairs.TryGetValue(dto.User.UID, out var connection))
            {
                return PairOperationResult.Fail($"Pair {dto.User.UID} not found.");
            }

            connection.UpdatePermissions(dto.Permissions, connection.OtherToSelfPermissions);
            return PairOperationResult.Ok();
        }
    }

    private PairConnection GetOrCreatePair(UserData user)
    {
        return GetOrCreatePair(user, out _);
    }

    private PairConnection GetOrCreatePair(UserData user, out bool created)
    {
        if (_pairs.TryGetValue(user.UID, out var connection))
        {
            created = false;
            return connection;
        }

        connection = new PairConnection(user);
        _pairs[user.UID] = connection;
        created = true;
        return connection;
    }

    private Syncshell GetOrCreateShell(GroupData group)
    {
        if (_groups.TryGetValue(group.GID, out var shell))
        {
            return shell;
        }

        var placeholder = new GroupFullInfoDto(
            group,
            new UserData(string.Empty),
            GroupPermissions.NoneSet,
            GroupUserPreferredPermissions.NoneSet,
            GroupPairUserInfo.None,
            new Dictionary<string, GroupPairUserInfo>(StringComparer.Ordinal),
            0);

        shell = new Syncshell(placeholder);
        _groups[group.GID] = shell;
        return shell;
    }

    private PairRegistration? TryRemovePairIfNoConnection(PairConnection connection)
    {
        if (connection.HasAnyConnection)
        {
            return null;
        }

        if (connection.IsOnline)
        {
            connection.SetOffline();
        }

        var userId = connection.User.UID;
        _pairs.Remove(userId);
        foreach (var shell in _groups.Values)
        {
            shell.Users.Remove(userId);
        }

        return new PairRegistration(new PairUniqueIdentifier(userId), connection.Ident);
    }

    public static PairConnection CreateFromFullData(UserFullPairDto dto)
    {
        var connection = new PairConnection(dto.User);
        connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions);
        connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus);

        foreach (var groupId in dto.Groups)
        {
            connection.EnsureGroupRelationship(groupId, null);
        }

        return connection;
    }

    public static PairConnection CreateFromPartialData(UserPairDto dto)
    {
        var connection = new PairConnection(dto.User);
        connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions);
        connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus);
        return connection;
    }

    public static GroupPairRelationship CreateGroupPairRelationshipFromFullInfo(string userUid, GroupFullInfoDto fullInfo)
    {
        return new GroupPairRelationship(fullInfo.Group.GID,
            fullInfo.GroupPairUserInfos.TryGetValue(userUid, out var info) && info != GroupPairUserInfo.None
                ? info
                : null);
    }
}
