﻿using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Security;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using ConVar;
using Facepunch;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using UnityEngine;
using GC = System.GC;
using Pool = Facepunch.Pool;

//Reference: System.Threading.Channels
//Reference: System.Net.Http
//Reference: Facepunch.Sqlite

namespace Oxide.Plugins;

[Info("ToxVoice", "Maxaki", "1.4.2")]
[Description("Transcribes voice chat to text and filters, logs, and alerts based on user-defined rules.")]
public class ToxVoice : RustPlugin
{
    private readonly VoiceNetworking _networking;
    private readonly PlayerVoiceSink _voiceSink;
    private readonly CancellationTokenSource _shutdownCts = new();

    private readonly ConfigurationFile _pluginConfig;
    private static readonly StringBuilder ToxVoiceCommandBuilder = new();

    private const string WhitelistPermission = "toxvoice.whitelist";
    private readonly HashSet<string> _whitelistedUsers = new();

    public ToxVoice()
    {
        _pluginConfig = ConfigurationFile.LoadConfiguration();
        _networking = new VoiceNetworking(_pluginConfig, _shutdownCts.Token);
        _voiceSink = new PlayerVoiceSink(_networking, _pluginConfig, _shutdownCts.Token);
    }

    private void Init()
    {
        permission.RegisterPermission(WhitelistPermission, this);
    }

    protected override void LoadDefaultMessages()
    {
        lang.RegisterMessages(new Dictionary<string, string>
        {
            ["InvalidCommand"] = "Invalid command. Usage: toxvoice <subcommand> <parameter>",
            ["ViolationsResetAll"] = "All player violations have been reset.",
            ["ViolationsResetPlayer"] = "Violations for player with SteamID {0} have been reset.",
            ["InvalidSteamId"] = "Invalid parameter. Please provide a valid SteamID or use 'all' to reset all violations.",
            ["InvalidSteamIdWhitelist"] = "Invalid parameter. Please provide a valid SteamID",
            ["WhitelistRemoved"] = "User({0}) removed from whitelist.",
            ["WhitelistAdded"] = "User({0}) added to whitelist."
        }, this);
    }

    private string Lang(string key, params object[] args) => string.Format(lang.GetMessage(key, this), args);

    private void OnPlayerConnected(BasePlayer player)
    {
        if (permission.UserHasPermission(player.UserIDString, WhitelistPermission))
        {
            _whitelistedUsers.Add(player.UserIDString);
        }
    }

    private void OnPlayerDisconnected(BasePlayer player, string reason)
    {
        _whitelistedUsers.Remove(player.UserIDString);
    }

    private void OnServerInitialized()
    {
        Rust.Global.Runner.StartCoroutine(HandlePlayerInitializations());

        ToxVoicePersistence.Init();

        Task.Run(_networking.StartAsync).ConfigureAwait(false);
        Task.Run(_voiceSink.StartAsync).ConfigureAwait(false);
    }

    private IEnumerator HandlePlayerInitializations()
    {
        var activePlayers = BasePlayer.activePlayerList;
        foreach (var basePlayer in activePlayers)
        {
            OnPlayerConnected(basePlayer);
            yield return null;
        }
    }

    private void OnPlayerVoice(BasePlayer player, ArraySegment<byte> data)
    {
        if (_whitelistedUsers.Contains(player.UserIDString))
            return;

        var voicePacket = SharedObjectPool<VoicePacket>.Get();
        voicePacket.Init(player.userID, player.displayName, data);
        _voiceSink.TryWrite(voicePacket);
    }

    private void Unload()
    {
        if (!_shutdownCts.IsCancellationRequested)
            _shutdownCts.Cancel();

        try
        {
            _voiceSink.Dispose();
        }
        catch
        {
            // ignored
        }

        try
        {
            _networking.Dispose();
        }
        catch
        {
            // ignored
        }

        ToxVoicePersistence.Close();
    }

    private void OnUserPermissionGranted(string playerId, string perm)
    {
        if (perm != WhitelistPermission)
            return;

        _whitelistedUsers.Add(playerId);
    }

    private void OnUserPermissionRevoked(string playerId, string perm)
    {
        if (perm != WhitelistPermission)
            return;

        _whitelistedUsers.Remove(playerId);
    }

    public static void UnloadPlugin(string reason)
    {
        Threading.QueueOnMainThread(() =>
        {
            Interface.Oxide.UnloadPlugin(nameof(ToxVoice));
            Logger.Error(reason);
        });
    }

    protected override void SaveConfig()
    {
        Config.WriteObject(_pluginConfig);
    }

    [ConsoleCommand("toxvoice")]
    private void ToxVoiceCommand(ConsoleSystem.Arg arg)
    {
        try
        {
            var args = arg.FullString.ToString().Split(' ');
            if (args.Length < 2)
            {
                arg.ReplyWith($"[ToxVoice] {Lang("InvalidCommand")}");
                return;
            }

            var subcommand = arg.GetString(0).ToLower();
            var parameter = arg.GetString(1);

            ToxVoiceCommandBuilder.AppendLine("{");

            switch (subcommand)
            {
                case "steam":
                    if (ulong.TryParse(parameter, out _))
                    {
                        var toxVoiceUserId = ToxVoicePersistence.GetToxVoiceIdFromSteamId(parameter);
                        if (!string.IsNullOrEmpty(toxVoiceUserId))
                        {
                            ToxVoiceCommandBuilder.AppendLine($"  \"SteamID\": {parameter},");
                            ToxVoiceCommandBuilder.AppendLine($"  \"ToxVoiceUserID\": \"{toxVoiceUserId}\"");
                        }
                        else
                        {
                            ToxVoiceCommandBuilder.AppendLine($"  \"Error\": \"No ToxVoiceUserID found for SteamID: {parameter}\"");
                        }
                    }
                    else
                    {
                        ToxVoiceCommandBuilder.AppendLine("  \"Error\": \"Invalid SteamID. Please provide a valid SteamID.\"");
                    }

                    break;

                case "id":
                    if (Guid.TryParse(parameter, out _))
                    {
                        var steamId = ToxVoicePersistence.GetSteamIdFromToxVoiceId(parameter);
                        if (!string.IsNullOrEmpty(steamId))
                        {
                            ToxVoiceCommandBuilder.AppendLine($"  \"ToxVoiceUserID\": \"{parameter}\",");
                            ToxVoiceCommandBuilder.AppendLine($"  \"SteamID\": {steamId}");
                        }
                        else
                        {
                            ToxVoiceCommandBuilder.AppendLine($"  \"Error\": \"No SteamID found for ToxVoiceUserID: {parameter}\"");
                        }
                    }
                    else
                    {
                        ToxVoiceCommandBuilder.AppendLine("  \"Error\": \"Invalid ToxVoiceUserID. Please provide a valid GUID.\"");
                    }

                    break;
                case "reset":
                    if (parameter == "all")
                    {
                        ToxVoicePersistence.ResetAllViolations();
                        arg.ReplyWith($"[ToxVoice] {Lang("ViolationsResetAll")}");
                        return;
                    }
                    else if (ulong.TryParse(parameter, out _))
                    {
                        ToxVoicePersistence.ResetPlayerViolation(parameter);
                        arg.ReplyWith($"[ToxVoice] {Lang("ViolationsResetPlayer", parameter)}");
                        return;
                    }
                    else
                    {
                        arg.ReplyWith($"[ToxVoice] {Lang("InvalidSteamId")}");
                        return;
                    }
                case "whitelist":
                    if (!ulong.TryParse(parameter, out _))
                    {
                        arg.ReplyWith($"[ToxVoice] {Lang("InvalidSteamIdWhitelist")}");
                        return;
                    }

                    if (permission.UserHasPermission(parameter, WhitelistPermission))
                    {
                        permission.RevokeUserPermission(parameter, WhitelistPermission);
                        arg.ReplyWith($"[ToxVoice] {Lang("WhitelistRemoved", parameter)}");
                        return;
                    }
                    else
                    {
                        permission.GrantUserPermission(parameter, WhitelistPermission, this);
                        arg.ReplyWith($"[ToxVoice] {Lang("WhitelistAdded", parameter)}");
                        return;
                    }
                default:
                    ToxVoiceCommandBuilder.AppendLine($"  \"Error\": \"Unknown subcommand: {subcommand}\"");
                    break;
            }

            ToxVoiceCommandBuilder.AppendLine("}");

            var jsonString = ToxVoiceCommandBuilder.ToString();
            arg.ReplyWith($"[ToxVoice]\n{jsonString}");
        }
        finally
        {
            ToxVoiceCommandBuilder.Clear();
        }
    }

    public class ConfigurationFile
    {
        private static readonly string ConfigFile = Path.Combine(Interface.Oxide.ConfigDirectory, "ToxVoice.json");

        public ConfigurationFile()
        { }

        public ToxVoiceConfiguration ToxVoice { get; set; } = new();
        public TranscriptionLogs TranscriptionLogs { get; set; } = new();
        public WeightConfiguration WeightConfiguration { get; set; } = new();
        public TriggerFilterConfiguration TriggerFilter { get; set; } = new();

        public static List<TriggerFilter> GetDefaultFilters() => new()
        {
            new TriggerFilter { Regex = false, Triggers = new List<string> { "word" }, Weight = 8 },
            new TriggerFilter { Regex = false, Triggers = new List<string> { "word1", "word2" }, Weight = 3 },
            new TriggerFilter { Regex = false, Triggers = new List<string> { "testing" }, Weight = 50 },
            new TriggerFilter { Regex = true, Triggers = new List<string> { @"\bword1\b.*\bword2\b.*\bword3\b" }, Weight = 8 }
        };

        public bool TryCreateDiscordLogsHttpClient(out DiscordHttpClient? discordHttpClient)
        {
            if (TranscriptionLogs.DiscordLog.Enabled)
            {
                if (Uri.TryCreate(TranscriptionLogs.DiscordLog.WebhookUrl, UriKind.Absolute, out var uri))
                {
                    discordHttpClient = new DiscordHttpClient(uri, TranscriptionLogs.DiscordLog.HideSteamId, TranscriptionLogs.ProximityLogs);
                    return true;
                }
            }

            discordHttpClient = null;
            return false;
        }

        public bool TryCreateTranscriptionFilter(out TranscriptionFilter? transcriptionFilter)
        {
            if (TriggerFilter.Enabled)
            {
                transcriptionFilter = new TranscriptionFilter(this);
                return true;
            }

            transcriptionFilter = default;
            return false;
        }

        public bool TryCreateDiscordAlertHttpClient(out DiscordHttpClient? discordHttpClient)
        {
            if (WeightConfiguration.DiscordWeightThreshold.Enabled)
            {
                if (Uri.TryCreate(WeightConfiguration.DiscordWeightThreshold.AlertWebhookUrl, UriKind.Absolute, out var uri))
                {
                    discordHttpClient = new DiscordHttpClient(uri, TranscriptionLogs.DiscordLog.HideSteamId, TranscriptionLogs.ProximityLogs);
                    return true;
                }
            }

            discordHttpClient = default;
            return false;
        }

        private static void SaveConfiguration(ConfigurationFile config)
        {
            var settings = new JsonSerializerSettings
            {
                DefaultValueHandling = DefaultValueHandling.Include,
                NullValueHandling = NullValueHandling.Ignore,
                Formatting = Formatting.Indented
            };

            var json = JsonConvert.SerializeObject(config, settings);

            File.WriteAllText(ConfigFile, json);
        }

        public static ConfigurationFile LoadConfiguration()
        {
            if (!File.Exists(ConfigFile))
            {
                var config = new ConfigurationFile();
                SaveConfiguration(config);
                return config;
            }

            var json = File.ReadAllText(ConfigFile);
            var settings = new JsonSerializerSettings
            {
                DefaultValueHandling = DefaultValueHandling.Populate,
                NullValueHandling = NullValueHandling.Ignore,
                ObjectCreationHandling = ObjectCreationHandling.Replace
            };

            var existingConfig = JsonConvert.DeserializeObject<ConfigurationFile>(json, settings) ?? new ConfigurationFile();
            SaveConfiguration(existingConfig);
            return existingConfig;
        }
    }

    public class RecipientInfo : Pool.IPooled
    {
        public ulong UserId { get; set; }
        public string DisplayName { get; set; } = string.Empty;
        public float Distance { get; set; }
        public long Timestamp { get; set; }

        public void Init(ulong userId, string displayName, float distance, long timestamp)
        {
            UserId = userId;
            DisplayName = displayName;
            Distance = distance;
            Timestamp = timestamp;
        }

        public void EnterPool()
        {
            UserId = 0;
            DisplayName = string.Empty;
            Distance = 0;
            Timestamp = 0;
        }

        public void LeavePool()
        { }
    }

    public class VoicePacket : Pool.IPooled
    {
        public ulong UserId { get; private set; }
        public string DisplayName { get; set; } = string.Empty;
        public byte[] Data { get; private set; } = Array.Empty<byte>();
        public List<RecipientInfo> Recipients { get; private set; } = new();

        public void Init(ulong userId, string displayName, ArraySegment<byte> data)
        {
            UserId = userId;
            DisplayName = displayName;
            if (data.Array == null || data.Count == 0)
            {
                Data = Array.Empty<byte>();
                return;
            }

            Data = new byte[data.Count];
            Buffer.BlockCopy(data.Array, data.Offset, Data, 0, data.Count);
        }

        public void EnterPool()
        {
            UserId = 0;
            DisplayName = string.Empty;
            Data = Array.Empty<byte>();
            foreach (var recipientInfo in Recipients)
            {
                SharedObjectPool<RecipientInfo>.Return(recipientInfo);
            }

            Recipients.Clear();
        }

        public void LeavePool()
        { }
    }

    public class VoiceSnapshot : Pool.IPooled
    {
        public ulong UserId { get; private set; }
        public Dictionary<string, string> Metadata { get; private set; } = new();
        public List<VoicePacket> VoicePackets { get; private set; } = new();
        public bool ProximityLogs { get; private set; }

        public void Init(ulong userId, Dictionary<string, string> metadata, List<VoicePacket> voicePackets, bool proximityLogs)
        {
            UserId = userId;
            foreach (var kvp in metadata)
            {
                Metadata[kvp.Key] = kvp.Value;
            }
            VoicePackets.AddRange(voicePackets);
            ProximityLogs = proximityLogs;
        }

        public void EnterPool()
        {
            UserId = 0;
            Metadata.Clear();
            foreach (var voicePacket in VoicePackets)
            {
                SharedObjectPool<VoicePacket>.Return(voicePacket);
            }
            VoicePackets.Clear();
            ProximityLogs = false;
        }

        public void LeavePool()
        { }
    }

    private class PlayerVoiceSink : IDisposable
    {
        private bool _disposed;
        private int _callbackRunning;

        private readonly VoiceNetworking _voiceNetworking;
        private readonly CancellationToken _shutdownToken;
        private readonly ConcurrentDictionary<ulong, PlayerVoiceContext> _playerVoiceContexts = new();

        private readonly Channel<VoicePacket> _voiceSink = Channel.CreateUnbounded<VoicePacket>();
        private readonly TimeSpan _idleThreshold = TimeSpan.FromMinutes(1);

        private readonly bool _proximityLogs;

        public PlayerVoiceSink(VoiceNetworking voiceNetworking, ConfigurationFile config, CancellationToken shutdownToken)
        {
            _voiceNetworking = voiceNetworking;
            _proximityLogs = config.TranscriptionLogs.ProximityLogs;
            _shutdownToken = shutdownToken;
        }

        public void TryWrite(VoicePacket voicePacket)
        {
            _voiceSink.Writer.TryWrite(voicePacket);
        }

        public async Task StartAsync()
        {
            await using var timer = new System.Threading.Timer(Callback, null, 1000, 1000);

            try
            {
                while (await _voiceSink.Reader.WaitToReadAsync(_shutdownToken).ConfigureAwait(false))
                {
                    while (_voiceSink.Reader.TryRead(out var voicePacket))
                    {
                        GetOrAddPlayerVoiceRecording(voicePacket.UserId, voicePacket.DisplayName).Enqueue(voicePacket);
                    }
                }
            }
            catch (OperationCanceledException)
            { }
            catch (Exception e)
            {
                Logger.Error($"Unexpected exception in voice sink: {e.Message}");
            }
        }

        private void Callback(object state)
        {
            if (_disposed)
                return;

            if (Interlocked.CompareExchange(ref _callbackRunning, 1, 0) != 0)
                return;

            try
            {
                var idleEntries = new List<ulong>();
                foreach (var playerVoiceContext in _playerVoiceContexts)
                {
                    if (playerVoiceContext.Value.IsIdle(_idleThreshold))
                    {
                        idleEntries.Add(playerVoiceContext.Key);
                        continue;
                    }

                    var snapshot = playerVoiceContext.Value.TakeSnapshot(_proximityLogs);
                    if (snapshot != null)
                    {
                        _voiceNetworking.TryWrite(snapshot);
                    }
                }

                foreach (var idleEntry in idleEntries)
                {
                    if (!_playerVoiceContexts.TryRemove(idleEntry, out var removedContext))
                        continue;

                    if (!removedContext.IsIdle(_idleThreshold))
                    {
                        _playerVoiceContexts.TryAdd(idleEntry, removedContext);
                        continue;
                    }

                    removedContext.Dispose();
                }
            }
            catch (Exception e)
            {
                Logger.Error($"Voice sink callback error: {e.Message}");
            }
            finally
            {
                Interlocked.Exchange(ref _callbackRunning, 0);
            }
        }

        private PlayerVoiceContext GetOrAddPlayerVoiceRecording(ulong userId, string displayName)
        {
            if (_playerVoiceContexts.TryGetValue(userId, out var playerVoiceRecording))
                return playerVoiceRecording;

            playerVoiceRecording = new PlayerVoiceContext(userId, displayName);
            _playerVoiceContexts[userId] = playerVoiceRecording;
            return playerVoiceRecording;
        }

        public void Dispose()
        {
            if (_disposed)
                return;

            _disposed = true;

            foreach (var playerVoiceContext in _playerVoiceContexts)
            {
                playerVoiceContext.Value.Dispose();
            }

            _playerVoiceContexts.Clear();
            _voiceSink.Writer.TryComplete();
        }
    }

    private static class Logger
    {
        private const string Prefix = "[ToxVoice]";

        internal static void Info(string message)
        {
            if (Thread.CurrentThread.ManagedThreadId == 1)
            {
                Debug.Log($"{Prefix} {message}");
                return;
            }

            Threading.QueueOnMainThread(() =>
            {
                Debug.Log($"{Prefix} {message}");
            });
        }

        internal static void Error(string message)
        {
            if (Thread.CurrentThread.ManagedThreadId == 1)
            {
                Debug.LogError($"{Prefix} {message}");
                return;
            }

            Threading.QueueOnMainThread(() =>
            {
                Debug.LogError($"{Prefix} {message}");
            });
        }

        internal static void Warning(string message)
        {
            if (Thread.CurrentThread.ManagedThreadId == 1)
            {
                Debug.LogWarning($"{Prefix} {message}");
                return;
            }

            Threading.QueueOnMainThread(() =>
            {
                Debug.LogWarning($"{Prefix} {message}");
            });
        }
    }

    private static class ToxVoicePersistence
    {
        private static readonly Facepunch.Sqlite.Database Identity = new();
        private static readonly ConcurrentDictionary<string, string> ToxVoiceUserIdCache = new();
        private static readonly object Lock = new();
        private static readonly string DataFile = Path.Combine(Interface.Oxide.DataDirectory, "ToxVoice.db");

        static ToxVoicePersistence()
        { }

        public static void Init()
        {
            lock (Lock)
            {
                Identity.Open(DataFile);
                Identity.Execute("CREATE TABLE IF NOT EXISTS users (steamid TEXT PRIMARY KEY, toxvoiceuserid TEXT)");
                Identity.Execute("CREATE TABLE IF NOT EXISTS violations (steamid TEXT PRIMARY KEY, violationcount INTEGER)");
            }
        }

        public static void Close()
        {
            try
            {
                lock (Lock)
                {
                    Identity.Close();
                }
            }
            catch (Exception exception)
            {
                Logger.Error("Error closing database: " + exception.Message);
            }
            finally
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
            }
        }

        public static void ResetAllViolations()
        {
            lock (Lock)
            {
                Identity.Execute("DELETE FROM violations");
            }
        }

        public static void ResetPlayerViolation(string steamId)
        {
            lock (Lock)
            {
                Identity.Execute("DELETE FROM violations WHERE steamid = ?", steamId);
            }
        }

        public static int IncrementViolationCount(string steamId)
        {
            lock (Lock)
            {
                var existingCount = Identity.Query<int, string>("SELECT violationcount FROM violations WHERE steamid = ?", steamId);
                if (existingCount > 0)
                {
                    Identity.Execute("UPDATE violations SET violationcount = violationcount + 1 WHERE steamid = ?", steamId);
                    return existingCount + 1;
                }

                Identity.Execute("INSERT INTO violations (steamid, violationcount) VALUES (?, 1)", steamId);
                return 1;
            }
        }

        public static string GetOrGenerateToxVoiceId(string playerID)
        {
            if (Identity == null)
            {
                throw new InvalidOperationException("Identity database is not initialized.");
            }

            var toxVoiceId = ToxVoiceUserIdCache.FirstOrDefault(x => x.Value == playerID).Key;
            if (!string.IsNullOrEmpty(toxVoiceId))
            {
                return toxVoiceId;
            }

            lock (Lock)
            {
                var toxVoiceUserId = Identity.Query<string, string>("SELECT toxvoiceuserid FROM users WHERE steamid = ?", playerID);
                if (!string.IsNullOrEmpty(toxVoiceUserId))
                {
                    ToxVoiceUserIdCache[toxVoiceUserId] = playerID;
                    return toxVoiceUserId;
                }

                toxVoiceId = GenerateId();
                Identity.Execute("INSERT INTO users (steamid, toxvoiceuserid) VALUES (?, ?)", playerID, toxVoiceId);
                ToxVoiceUserIdCache[toxVoiceId] = playerID;
            }

            return toxVoiceId;
        }

        public static string GetSteamIdFromToxVoiceIdCache(string toxVoiceUserId) => ToxVoiceUserIdCache.GetValueOrDefault(toxVoiceUserId, string.Empty);

        public static string GetSteamIdFromToxVoiceId(string toxVoiceUserId)
        {
            if (ToxVoiceUserIdCache.TryGetValue(toxVoiceUserId, out var steamId))
            {
                return steamId;
            }

            lock (Lock)
            {
                steamId = Identity.Query<string, string>("SELECT steamid FROM users WHERE toxvoiceuserid = ?", toxVoiceUserId);
                if (!string.IsNullOrEmpty(steamId))
                {
                    ToxVoiceUserIdCache[toxVoiceUserId] = steamId;
                }
            }

            return steamId;
        }

        public static string GetToxVoiceIdFromSteamId(string userId)
        {
            var toxVoiceUserId = ToxVoiceUserIdCache.FirstOrDefault(x => x.Value == userId).Key;
            if (!string.IsNullOrEmpty(toxVoiceUserId))
            {
                return toxVoiceUserId;
            }

            lock (Lock)
            {
                toxVoiceUserId = Identity.Query<string, string>("SELECT toxvoiceuserid FROM users WHERE steamid = ?", userId);
                if (!string.IsNullOrEmpty(toxVoiceUserId))
                {
                    ToxVoiceUserIdCache[toxVoiceUserId] = userId;
                }
            }

            return toxVoiceUserId;
        }

        private static string GenerateId() => Guid.NewGuid().ToString("N");
    }

    public class TranscriptionFilter
    {
        public readonly List<TriggerFilter> Filters;

        public TranscriptionFilter(ConfigurationFile configurationFile)
        {
            Filters = configurationFile.TriggerFilter.Filters;
        }

        public int GetViolatedFilterWeight(string text) => GetViolatedFilterWeightCore(text, Filters);

        private static int GetViolatedFilterWeightCore(string text, List<TriggerFilter> filters)
        {
            var totalWeight = 0;
            foreach (var filter in filters)
            {
                if (filter.Regex)
                {
                    if (!IsRegexMatch(text, filter.Triggers))
                        continue;
                }
                else
                {
                    if (!IsWordListContained(text, filter))
                        continue;
                }

                totalWeight += filter.Weight;
            }

            return totalWeight;
        }

        private static bool IsRegexMatch(string text, List<string> regexPatterns)
        {
            foreach (var pattern in regexPatterns)
            {
                try
                {
                    if (!Regex.IsMatch(text, pattern, RegexOptions.IgnoreCase))
                    {
                        return false;
                    }
                }
                catch (Exception)
                {
                    Logger.Warning("Error in regex pattern: " + pattern);
                }
            }

            return true;
        }

        private static bool IsWordListContained(ReadOnlySpan<char> text, TriggerFilter triggerFilter)
        {
            foreach (var word in triggerFilter.Triggers)
            {
                if (!ContainsWord(text, word.AsSpan()))
                {
                    return false;
                }
            }

            return true;
        }

        private static bool ContainsWord(ReadOnlySpan<char> text, ReadOnlySpan<char> word)
        {
            var textLength = text.Length;
            var wordLength = word.Length;

            for (var i = 0; i <= textLength - wordLength; i++)
            {
                var substring = text.Slice(i, wordLength);
                if (substring.Length == wordLength && substring.Equals(word, StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }
            }

            return false;
        }
    }

    public class TranscriptionResult : Pool.IPooled
    {
        public Dictionary<string, string> Metadata { get; set; } = new();
        public byte[] AudioData { get; set; } = Array.Empty<byte>();

        public void EnterPool()
        {
            Metadata.Clear();
            AudioData = Array.Empty<byte>();
        }

        public void LeavePool()
        { }
    }

    public sealed class DiscordWorkItem
    {
        public string SteamId { get; init; } = string.Empty;
        public string UserId { get; init; } = string.Empty;
        public string DisplayName { get; init; } = string.Empty;
        public string Position { get; init; } = string.Empty;
        public string Text { get; init; } = string.Empty;
        public string RecipientsJson { get; init; } = string.Empty;
        public byte[] AudioData { get; init; } = Array.Empty<byte>();
        public int ViolatedFilterWeight { get; init; }
        public int Violations { get; init; }
        public bool UseAlertWebhook { get; init; }
    }

    public readonly struct DiscordSendResult
    {
        public bool IsSuccess { get; }
        public HttpStatusCode StatusCode { get; }

        private DiscordSendResult(bool isSuccess, HttpStatusCode statusCode)
        {
            IsSuccess = isSuccess;
            StatusCode = statusCode;
        }

        public static DiscordSendResult Success(HttpStatusCode statusCode) => new(true, statusCode);
        public static DiscordSendResult Failure(HttpStatusCode statusCode) => new(false, statusCode);
    }

    public class DiscordHttpClient : IDisposable
    {
        private bool _disposed;
        private bool _permanentlyDisabled;
        private readonly bool _hideSteamId;
        private readonly bool _proximityLogs;
        private readonly Uri _webhookUri;
        private readonly HttpClient _httpClient;
        private readonly JsonSerializerSettings _recipientSerializer = new()
        {
            Converters = new List<JsonConverter> { new PooledRecipientInfoConverter() }
        };

        public DiscordHttpClient(Uri webhookUri, bool hideSteamId, bool proximityLogs)
        {
            _hideSteamId = hideSteamId;
            _proximityLogs = proximityLogs;
            _webhookUri = webhookUri;
            _httpClient = new HttpClient();
        }

        private MultipartFormDataContent CreateMultipartContent(DiscordWorkItem workItem)
        {
            var content = new MultipartFormDataContent();

            var userId = workItem.UserId;
            var displayName = workItem.DisplayName;
            var position = workItem.Position;
            var text = workItem.Text;

            if (!_hideSteamId && !string.IsNullOrEmpty(workItem.SteamId))
            {
                userId = workItem.SteamId;
            }

            var multiPartBuilder = Pool.Get<StringBuilder>();
            try
            {
                multiPartBuilder.AppendLine("```md");
                multiPartBuilder.AppendLine("# Transcript");
                multiPartBuilder.AppendLine($"- Player: {displayName} ({userId})");
                multiPartBuilder.AppendLine($"- Position: {position}");
                multiPartBuilder.AppendLine("- Text: {text}");

                if (workItem.ViolatedFilterWeight > 0)
                {
                    multiPartBuilder.AppendLine("# Moderation");
                    multiPartBuilder.AppendLine($"- Weight: {workItem.ViolatedFilterWeight}");
                    if (workItem.Violations > 0)
                        multiPartBuilder.AppendLine($"- Violations: {workItem.Violations}");
                }

                if (_proximityLogs && !string.IsNullOrEmpty(workItem.RecipientsJson))
                {
                    multiPartBuilder.AppendLine("# Proximity");
                    AppendRecipientInfo(multiPartBuilder, workItem.RecipientsJson);
                }

                multiPartBuilder.Append("```");

                var contentWithoutText = multiPartBuilder.ToString();
                var availableSpace = 2000 - contentWithoutText.Length + 6;

                if (text.Length > availableSpace)
                {
                    text = text.Substring(0, availableSpace - 3) + "...";
                }

                var finalContent = contentWithoutText.Replace("{text}", text);

                var textContent = new StringContent(finalContent);
                content.Add(textContent, "content");

                var fileContent = new ByteArrayContent(workItem.AudioData);
                fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("audio/mp3");
                content.Add(fileContent, "file", $"{userId}.mp3");

                return content;
            }
            finally
            {
                Pool.FreeUnmanaged(ref multiPartBuilder);
            }
        }

        private void AppendRecipientInfo(StringBuilder sb, string recipientsJson)
        {
            try
            {
                var recipients = JsonConvert.DeserializeObject<List<RecipientInfo>>(recipientsJson, _recipientSerializer);

                if (recipients is { Count: > 0 })
                {
                    foreach (var recipient in recipients)
                    {
                        sb.AppendLine($"- Player: {recipient.DisplayName} ({recipient.UserId})");
                        sb.AppendLine($"  Distance: {recipient.Distance:F2}");
                        SharedObjectPool<RecipientInfo>.Return(recipient);
                    }
                }
                else
                {
                    sb.AppendLine("- No recipients found.");
                }
            }
            catch (Exception ex)
            {
                sb.AppendLine($"- Error parsing recipient data: {ex.Message}");
            }
        }

        public async Task<DiscordSendResult> SendMessageWithRetryAsync(DiscordWorkItem workItem, CancellationToken cancellationToken)
        {
            const int maxRetries = 3;
            const int baseRetryDelay = 1000;

            if (_permanentlyDisabled)
                return DiscordSendResult.Failure(HttpStatusCode.NotFound);

            using var content = CreateMultipartContent(workItem);

            var pendingRateLimitDelay = TimeSpan.Zero;
            var lastStatus = HttpStatusCode.InternalServerError;

            for (var retry = 0; retry < maxRetries; retry++)
            {
                if (cancellationToken.IsCancellationRequested)
                    return DiscordSendResult.Failure(HttpStatusCode.InternalServerError);

                if (pendingRateLimitDelay > TimeSpan.Zero)
                {
                    Logger.Info($"Proactively waiting {pendingRateLimitDelay.TotalSeconds:F2} seconds to avoid rate limit.");
                    await Task.Delay(pendingRateLimitDelay, cancellationToken).ConfigureAwait(false);
                    pendingRateLimitDelay = TimeSpan.Zero;
                }

                try
                {
                    using var response = await _httpClient.PostAsync(_webhookUri, content, cancellationToken).ConfigureAwait(false);
                    lastStatus = response.StatusCode;

                    if (response.IsSuccessStatusCode)
                        return DiscordSendResult.Success(response.StatusCode);

                    if (IsRateLimited(response))
                    {
                        if (IsGlobalRateLimit(response))
                            Logger.Warning("Discord global rate limit triggered.");

                        var resetAfter = GetRateLimitResetAfter(response);
                        Logger.Warning($"Discord rate limited. Retrying after {resetAfter.TotalSeconds:F2} seconds.");
                        await Task.Delay(resetAfter, cancellationToken).ConfigureAwait(false);
                        continue;
                    }

                    if (IsNonRetriableClientError(response))
                    {
                        if ((int)response.StatusCode == 404)
                        {
                            _permanentlyDisabled = true;
                            Logger.Error("Discord webhook returned 404 — invalid or deleted. Disabling client permanently to avoid Discord temporary restrictions.");
                        }
                        else
                        {
                            Logger.Error($"Discord webhook returned non-retriable status {(int)response.StatusCode}. Aborting.");
                        }
                        return DiscordSendResult.Failure(response.StatusCode);
                    }

                    Logger.Warning($"Discord request failed with status {response.StatusCode}. Retry attempt: {retry + 1}");

                    if (IsBucketExhausted(response))
                        pendingRateLimitDelay = GetRateLimitResetAfter(response);
                }
                catch (OperationCanceledException)
                {
                    throw;
                }
                catch (Exception exception)
                {
                    Logger.Warning($"Failed to upload discord file. Retry attempt: {retry + 1}\n{exception.Message}");
                }

                var delay = baseRetryDelay * (int)Math.Pow(2, retry);
                await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
            }

            return DiscordSendResult.Failure(lastStatus);
        }

        private static TimeSpan GetRateLimitResetAfter(HttpResponseMessage response)
        {
            if (response.Headers.TryGetValues("Retry-After", out var retryAfterValues) &&
                double.TryParse(retryAfterValues.FirstOrDefault(), NumberStyles.Float, CultureInfo.InvariantCulture, out var retryAfterSeconds))
            {
                return TimeSpan.FromSeconds(retryAfterSeconds);
            }

            if (response.Headers.TryGetValues("X-RateLimit-Reset-After", out var resetAfterValues) &&
                double.TryParse(resetAfterValues.FirstOrDefault(), NumberStyles.Float, CultureInfo.InvariantCulture, out var resetAfterSecondsHeader))
            {
                return TimeSpan.FromSeconds(resetAfterSecondsHeader);
            }

            // X-RateLimit-Reset is unix epoch seconds, can include decimals for ms precision
            if (response.Headers.TryGetValues("X-RateLimit-Reset", out var resetValues) &&
                double.TryParse(resetValues.FirstOrDefault(), NumberStyles.Float, CultureInfo.InvariantCulture, out var resetTimestamp))
            {
                var resetTime = DateTimeOffset.FromUnixTimeMilliseconds((long)(resetTimestamp * 1000));
                var timeUntilReset = resetTime - DateTimeOffset.UtcNow;

                return timeUntilReset > TimeSpan.Zero ? timeUntilReset : TimeSpan.FromSeconds(1);
            }

            return TimeSpan.FromSeconds(5);
        }

        private static bool IsBucketExhausted(HttpResponseMessage response)
        {
            return response.Headers.TryGetValues("X-RateLimit-Remaining", out var remainingValues) &&
                   int.TryParse(remainingValues.FirstOrDefault(), out var remaining) &&
                   remaining <= 0;
        }

        private static bool IsGlobalRateLimit(HttpResponseMessage response)
        {
            return response.Headers.TryGetValues("X-RateLimit-Global", out var values) &&
                   bool.TryParse(values.FirstOrDefault(), out var isGlobal) && isGlobal;
        }

        private static bool IsNonRetriableClientError(HttpResponseMessage response)
        {
            // 4xx errors are client-side and won't succeed on retry — except 408 (Request Timeout)
            // and 429 (Rate Limited) which are handled separately. Retrying these wastes Discord's
            // Cloudflare invalid-request budget (10K/10min triggers a temporary IP ban).
            var status = (int)response.StatusCode;
            return status >= 400 && status < 500 && status != 408 && status != 429;
        }

        private static bool IsRateLimited(HttpResponseMessage response)
        {
            if (response.StatusCode == HttpStatusCode.TooManyRequests)
                return true;

            if (response.Headers.TryGetValues("X-RateLimit-Remaining", out var values) &&
                values.FirstOrDefault() == "0")
            {
                return response.Headers.Contains("X-RateLimit-Reset-After") ||
                       response.Headers.Contains("X-RateLimit-Reset");
            }

            return false;
        }

        public void Dispose()
        {
            if (_disposed)
                return;

            _disposed = true;
            _httpClient.Dispose();
        }
    }

    private class PooledRecipientInfoConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType) => objectType == typeof(RecipientInfo);

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var jObject = JObject.Load(reader);
            var recipientInfo = SharedObjectPool<RecipientInfo>.Get();

            recipientInfo.Init(
                jObject["UserId"].Value<ulong>(),
                jObject["DisplayName"].Value<string>(),
                jObject["Distance"].Value<float>(),
                jObject["Timestamp"].Value<long>()
            );

            return recipientInfo;
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException("Writing to JSON is not implemented for this converter.");
        }
    }

    public class ToxVoiceConfiguration
    {
        public string Token { get; set; } = "18cf2506-6619-4d0f-b1c5-898211f079f3";

        public bool IsValid() => Guid.TryParse(Token, out _);
    }

    public class TranscriptionLogs
    {
        [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
        public bool ProximityLogs { get; set; } = false;
        public DiscordLogs DiscordLog { get; set; } = new();
        public ConsoleLogs ConsoleLog { get; set; } = new();

        public class DiscordLogs
        {
            public bool Enabled { get; set; } = false;
            public bool HideSteamId { get; set; } = false;
            public string WebhookUrl { get; set; } = "YOUR_DISCORD_WEBHOOK_URL";
        }

        public class ConsoleLogs
        {
            public bool Enabled { get; set; } = true;
        }
    }

    public class WeightConfiguration
    {
        [JsonProperty(PropertyName = "DiscordWeightThreshold", ObjectCreationHandling = ObjectCreationHandling.Replace)]
        public DiscordWeightThreshold DiscordWeightThreshold { get; set; } = new();

        [JsonProperty(PropertyName = "ViolationWeightThreshold", ObjectCreationHandling = ObjectCreationHandling.Replace)]
        public ViolationWeightThreshold ViolationWeightThreshold { get; set; } = new();
    }

    public class DiscordWeightThreshold
    {
        public bool Enabled { get; set; } = false;
        public int TriggerAlertWeightThreshold { get; set; } = 20;
        public string AlertWebhookUrl { get; set; } = "WEBHOOK_URL";
    }

    public class TriggerFilter
    {
        public int Weight { get; set; }
        public bool Regex { get; set; }
        public List<string> Triggers { get; set; } = new();
    }

    public class TriggerFilterConfiguration
    {
        public bool Enabled { get; set; } = false;

        [JsonProperty(PropertyName = "TriggerFilters", ObjectCreationHandling = ObjectCreationHandling.Replace)]
        public List<TriggerFilter> Filters { get; set; } = ConfigurationFile.GetDefaultFilters();
    }

    public class ViolationWeightThreshold
    {
        public bool Enabled { get; set; } = false;
        public int TriggerActionWeightThreshold { get; set; } = 10;
        [JsonProperty(PropertyName = "ViolationActions", ObjectCreationHandling = ObjectCreationHandling.Replace)]
        public Dictionary<int, ViolationAction> ViolationActions { get; set; } = new()
        {
            { 1, new ViolationAction("warn {steamid} \"First warning for violating the rules\"", 30) },
            { 2, new ViolationAction("warn {steamid} \"Second warning for violating the rules\"", 30) },
            { 3, new ViolationAction("mute {steamid} 30s \"Muted for repeated rule violations\"", 30) },
            { 4, new ViolationAction("mute {steamid} 1m \"Muted for continued rule violations\"", 60) },
            { 5, new ViolationAction("mute {steamid} 5m \"Muted for persistent rule violations\"", 300) },
            { 6, new ViolationAction("mute {steamid} 1h \"Muted for ongoing rule violations\"", 3600) },
            { 7, new ViolationAction("mute {steamid} 3h \"Muted for frequent rule violations\"", 10800) },
            { 8, new ViolationAction("mute {steamid} 12h \"Muted for excessive rule violations\"", 43200) },
            { 9, new ViolationAction("mute {steamid} 1d \"Muted for numerous rule violations\"", 86400) },
            { 10, new ViolationAction("ban {steamid} 1d \"Banned for repeated and severe rule violations\"", 86400) }
        };

        public ViolationAction GetViolationAction(int violationCount)
        {
            if (ViolationActions.TryGetValue(violationCount, out var violationAction))
            {
                return violationAction;
            }

            return ViolationActions[ViolationActions.Keys.Max()];
        }
    }

    public class ViolationAction
    {
        [JsonProperty("Action")]
        public string Action { get; set; }

        [JsonProperty("CooldownSeconds")]
        public int CooldownSeconds { get; set; }

        public ViolationAction(string action, int cooldownSeconds)
        {
            Action = action;
            CooldownSeconds = cooldownSeconds;
        }
    }

    public class PlayerVoiceContext : IDisposable
    {
        private readonly ulong _userId;
        private bool _disposed;
        private long _lastProcessedTimeTicks;
        private const long TimeoutThresholdTicks = TimeSpan.TicksPerSecond * 5;
        private int _recording;
        private readonly ConcurrentQueue<VoicePacket> _voiceQueue = new();
        private Dictionary<string, string> Metadata { get; set; } = new();

        public PlayerVoiceContext(ulong userId, string displayName)
        {
            _userId = userId;
            var toxVoiceUserId = ToxVoicePersistence.GetOrGenerateToxVoiceId(userId.ToString());
            Metadata["ToxVoiceUserId"] = toxVoiceUserId;
            Metadata["DisplayName"] = displayName;
            Metadata["Version"] = "1.4.1";
        }

        public void Enqueue(VoicePacket voicePacket)
        {
            Interlocked.Exchange(ref _recording, 1);
            Interlocked.Exchange(ref _lastProcessedTimeTicks, DateTime.UtcNow.Ticks);
            _voiceQueue.Enqueue(voicePacket);
        }

        public bool IsIdle(TimeSpan idleThreshold)
        {
            if (Interlocked.CompareExchange(ref _recording, 0, 0) == 1)
                return false;

            var lastProcessedTicks = Interlocked.Read(ref _lastProcessedTimeTicks);
            return DateTime.UtcNow.Ticks - lastProcessedTicks >= idleThreshold.Ticks;
        }

        public VoiceSnapshot? TakeSnapshot(bool proximityLogs)
        {
            if (Interlocked.CompareExchange(ref _recording, 0, 0) == 0)
                return null;

            var count = _voiceQueue.Count;

            var lastProcessedTicks = Interlocked.Read(ref _lastProcessedTimeTicks);
            var timeoutReached = DateTime.UtcNow.Ticks - lastProcessedTicks >= TimeoutThresholdTicks;

            if (count <= 12 && !timeoutReached)
                return null;

            if (count <= 600 && (count <= 12 || !timeoutReached))
                return null;

            var packets = new List<VoicePacket>();
            while (_voiceQueue.TryDequeue(out var packet))
            {
                if (packet.Data.Length > 12)
                    packets.Add(packet);
            }

            if (packets.Count == 0)
                return null;

            Interlocked.Exchange(ref _recording, 0);
            Interlocked.Exchange(ref _lastProcessedTimeTicks, DateTime.UtcNow.Ticks);

            var snapshot = SharedObjectPool<VoiceSnapshot>.Get();
            snapshot.Init(_userId, Metadata, packets, proximityLogs);
            return snapshot;
        }

        public void Dispose()
        {
            if (_disposed) return;
            _disposed = true;
            _voiceQueue.Clear();
        }
    }

    public class RecipientList : Pool.IPooled
    {
        public readonly List<RecipientInfo> Items = new();

        public void EnterPool()
        {
            foreach (var item in Items)
                SharedObjectPool<RecipientInfo>.Return(item);

            Items.Clear();
        }

        public void LeavePool()
        { }
    }

    private static class SphereHelper
    {
        public static RecipientList GetRecipientsWithin(BasePlayer player)
        {
            var recipientList = SharedObjectPool<RecipientList>.Get();

            if (Thread.CurrentThread.ManagedThreadId != 1)
                return recipientList;

            var distance = 50f;
            if (player.HasPlayerFlag(BasePlayer.PlayerFlags.VoiceRangeBoost))
            {
                distance += Voice.voiceRangeBoostAmount;
            }

            var squaredDistance = distance * distance;
            var subscribers = BaseNetworkable.GlobalNetworkGroup.subscribers;
            for (var i = 0; i < subscribers.Count; i++)
            {
                var connection = subscribers[i];
                if (!connection.active)
                    continue;

                if (connection.player is not BasePlayer basePlayer)
                    continue;

                var sqrDist = player.SqrDistance(basePlayer);
                if (sqrDist > squaredDistance)
                    continue;

                if (connection.userid == player.userID)
                    continue;

                var recipientInfo = SharedObjectPool<RecipientInfo>.Get();
                recipientInfo.Init(connection.userid, basePlayer.displayName, Mathf.Sqrt(sqrDist), DateTime.UtcNow.Ticks);
                recipientList.Items.Add(recipientInfo);
            }

            return recipientList;
        }
    }

    private static class SharedObjectPool<T> where T : class, Pool.IPooled, new()
    {
        private static readonly ConcurrentQueue<T> Pool = new();

        public static T Get()
        {
            if (!Pool.TryDequeue(out var item))
                return new T();

            item.LeavePool();
            return item;
        }

        public static void Return(T? item)
        {
            if (item is null)
                return;

            item.EnterPool();
            Pool.Enqueue(item);
        }
    }

    private static class ToxVoiceViolationCooldownCache
    {
        private static readonly Dictionary<string, long> PlayerViolationCooldowns = new();

        public static void SetPlayerCooldown(string playerId, int cooldownSeconds)
        {
            if (string.IsNullOrEmpty(playerId))
                return;

            var expirationTicks = DateTime.UtcNow.AddSeconds(cooldownSeconds).Ticks;
            PlayerViolationCooldowns[playerId] = expirationTicks;
        }

        public static bool IsPlayerOnCooldown(string playerId)
        {
            if (!PlayerViolationCooldowns.TryGetValue(playerId, out var expirationTicks))
                return false;

            if (DateTime.UtcNow.Ticks < expirationTicks)
                return true;

            PlayerViolationCooldowns.Remove(playerId);
            return false;
        }
    }

    public class TranscriptionLogSink : IDisposable
    {
        private bool _disposed;
        private readonly DiscordHttpClient? _discordAlertLogsHttpClient;
        private readonly DiscordHttpClient? _defaultDiscordClient;
        private readonly TranscriptionFilter? _transcriptionFilter;
        private readonly ViolationWeightThreshold _violationWeightThreshold;
        private readonly int _discordAlertThreshold;
        private readonly bool _consoleLogsEnabled;
        private readonly Channel<TranscriptionResult> _transcriptionSink = Channel.CreateUnbounded<TranscriptionResult>();
        private readonly Channel<DiscordWorkItem> _discordSink = Channel.CreateUnbounded<DiscordWorkItem>();

        public TranscriptionLogSink(ConfigurationFile configuration, CancellationToken abortToken)
        {
            if (configuration.TryCreateDiscordLogsHttpClient(out _defaultDiscordClient))
            { }

            if (configuration.TryCreateDiscordAlertHttpClient(out _discordAlertLogsHttpClient))
            {
                _discordAlertThreshold = configuration.WeightConfiguration.DiscordWeightThreshold.TriggerAlertWeightThreshold;
            }

            if (!configuration.TryCreateTranscriptionFilter(out _transcriptionFilter))
            { }

            _consoleLogsEnabled = configuration.TranscriptionLogs.ConsoleLog.Enabled;
            _violationWeightThreshold = configuration.WeightConfiguration.ViolationWeightThreshold;

            _ = TranscriptionSinkTask(abortToken);
            _ = DiscordSinkTask(abortToken);
        }

        public void TryWrite(TranscriptionResult transcription) => _transcriptionSink.Writer.TryWrite(transcription);

        private async Task TranscriptionSinkTask(CancellationToken cancellationToken)
        {
            while (await _transcriptionSink.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false))
            {
                while (_transcriptionSink.Reader.TryRead(out var transcription))
                {
                    try
                    {
                        if (cancellationToken.IsCancellationRequested)
                            return;

                        var text = transcription.Metadata["Text"];
                        var steamId = ToxVoicePersistence.GetSteamIdFromToxVoiceIdCache(transcription.Metadata["ToxVoiceUserId"]);

                        var violatedFilterWeight = 0;
                        if (_transcriptionFilter is not null)
                        {
                            violatedFilterWeight = _transcriptionFilter.GetViolatedFilterWeight(text);
                        }

                        var violations = 0;
                        var action = string.Empty;
                        if (_violationWeightThreshold.Enabled)
                        {
                            if (_violationWeightThreshold.TriggerActionWeightThreshold <= violatedFilterWeight)
                            {
                                violations = ToxVoicePersistence.IncrementViolationCount(steamId);
                                var cachedCooldown = ToxVoiceViolationCooldownCache.IsPlayerOnCooldown(steamId);
                                if (!cachedCooldown)
                                {
                                    var violationAction = _violationWeightThreshold.GetViolationAction(violations);
                                    action = violationAction.Action.Replace("{steamid}", steamId);
                                    ToxVoiceViolationCooldownCache.SetPlayerCooldown(steamId, violationAction.CooldownSeconds);
                                }
                            }
                        }

                        var position = transcription.GetString("Position");
                        var recipients = transcription.GetString("Recipients");

                        Threading.QueueOnMainThread(() =>
                        {
                            if (!string.IsNullOrEmpty(action))
                                ConsoleSystem.Run(ConsoleSystem.Option.Server, action);

                            if (_consoleLogsEnabled)
                                DebugEx.Log($"[VOICE] [{steamId}] : {text}");

                            Interface.CallHook("OnPlayerVoiceText", steamId, text, position, recipients);
                        });

                        if (_discordAlertLogsHttpClient is not null || _defaultDiscordClient is not null)
                        {
                            var workItem = new DiscordWorkItem
                            {
                                SteamId = steamId,
                                UserId = transcription.GetString("ToxVoiceUserId"),
                                DisplayName = transcription.GetString("DisplayName"),
                                Position = position,
                                Text = text,
                                RecipientsJson = recipients,
                                AudioData = transcription.AudioData,
                                ViolatedFilterWeight = violatedFilterWeight,
                                Violations = violations,
                                UseAlertWebhook = _discordAlertLogsHttpClient is not null && _discordAlertThreshold <= violatedFilterWeight
                            };

                            _discordSink.Writer.TryWrite(workItem);
                        }
                    }
                    catch (OperationCanceledException)
                    {
                        return;
                    }
                    catch (Exception exception)
                    {
                        Logger.Error($"Unexpected transcription log exception: {exception.Message}");
                    }
                    finally
                    {
                        SharedObjectPool<TranscriptionResult>.Return(transcription);
                    }
                }
            }
        }

        private async Task DiscordSinkTask(CancellationToken cancellationToken)
        {
            while (await _discordSink.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false))
            {
                while (_discordSink.Reader.TryRead(out var workItem))
                {
                    try
                    {
                        if (cancellationToken.IsCancellationRequested)
                            return;

                        var messageSent = false;

                        if (workItem.UseAlertWebhook && _discordAlertLogsHttpClient is not null)
                        {
                            var result = await _discordAlertLogsHttpClient.SendMessageWithRetryAsync(workItem, cancellationToken).ConfigureAwait(false);
                            if (result.IsSuccess)
                            {
                                messageSent = true;
                            }
                            else
                            {
                                Logger.Warning($"Failed to upload discord file. Status code: {result.StatusCode}");
                            }
                        }

                        if (!messageSent && _defaultDiscordClient is not null)
                        {
                            var result = await _defaultDiscordClient.SendMessageWithRetryAsync(workItem, cancellationToken).ConfigureAwait(false);
                            if (!result.IsSuccess)
                            {
                                Logger.Warning($"Failed to upload discord file to default webhook. Status code: {result.StatusCode}");
                            }
                        }
                    }
                    catch (OperationCanceledException)
                    {
                        return;
                    }
                    catch (Exception exception)
                    {
                        Logger.Error($"Unexpected discord sink exception: {exception.Message}");
                    }
                }
            }
        }

        public void Dispose()
        {
            if (_disposed)
                return;

            _disposed = true;
            _transcriptionSink.Writer.TryComplete();
            _discordSink.Writer.TryComplete();
            _discordAlertLogsHttpClient?.Dispose();
            _defaultDiscordClient?.Dispose();
        }
    }

    private readonly struct Result<TValue, TError>
    {
        public readonly TValue Value;
        public readonly TError Error;
        public readonly bool IsSuccess;

        private Result(TValue value, TError error, bool isSuccess)
        {
            Value = value;
            Error = error;
            IsSuccess = isSuccess;
        }

        public static Result<TValue, TError> Ok(TValue value) => new(value, default!, true);
        public static Result<TValue, TError> Fail(TError error) => new(default!, error, false);
    }

    private enum ConnectionErrorKind
    {
        Fatal,
        InsufficientCredits,
        Transient
    }

    private readonly struct ConnectionError
    {
        public readonly ConnectionErrorKind Kind;
        public readonly string Message;

        private ConnectionError(ConnectionErrorKind kind, string message)
        {
            Kind = kind;
            Message = message;
        }

        public static ConnectionError Fatal(string message) => new(ConnectionErrorKind.Fatal, message);
        public static ConnectionError Credits() => new(ConnectionErrorKind.InsufficientCredits, "Insufficient credits. Purchase more at toxvoice.com");
        public static ConnectionError Transient(string message) => new(ConnectionErrorKind.Transient, message);
    }

    private sealed class ToxVoiceWebSocket : IDisposable
    {
        private readonly WebSocket _webSocket;
        private readonly Stream _stream;
        private readonly Socket _socket;
        private bool _disposed;

        public ToxVoiceWebSocket(WebSocket webSocket, Stream stream, Socket socket)
        {
            _webSocket = webSocket ?? throw new ArgumentNullException(nameof(webSocket));
            _stream = stream ?? throw new ArgumentNullException(nameof(stream));
            _socket = socket ?? throw new ArgumentNullException(nameof(socket));
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (_disposed)
                return;

            _disposed = true;

            if (disposing)
            {
                CloseWebSocket();
                DisposeStream();
                CloseAndDisposeSocket();
            }
        }

        private void CloseWebSocket()
        {
            if (_webSocket.State == WebSocketState.Open)
            {
                try
                {
                    _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disposing", CancellationToken.None)
                        .ConfigureAwait(false)
                        .GetAwaiter()
                        .GetResult();
                }
                catch (Exception ex)
                {
                    Logger.Warning($"Closing WebSocket Exception: {ex.Message}");
                }
            }

            try
            {
                _webSocket.Dispose();
            }
            catch
            { }
        }

        private void DisposeStream()
        {
            try
            {
                if (_stream is IDisposable disposableStream)
                {
                    disposableStream.Dispose();
                }
                else
                {
                    _stream.DisposeAsync()
                        .AsTask()
                        .ConfigureAwait(false)
                        .GetAwaiter()
                        .GetResult();
                }
            }
            catch (Exception ex)
            {
                Logger.Warning($"Disposing stream failed: {ex.Message}");
            }
        }

        private void CloseAndDisposeSocket()
        {
            try
            {
                if (_socket.Connected)
                {
                    _socket.Shutdown(SocketShutdown.Both);
                }

                _socket.Close();
            }
            catch (Exception)
            { }
            finally
            {
                try
                {
                    _socket.Dispose();
                }
                catch
                { }
            }
        }

        public Task<WebSocketReceiveResult> ReceiveAsync(byte[] buffer, CancellationToken cancellationToken) =>
            _webSocket.ReceiveAsync(buffer, cancellationToken);

        public Task CloseAsync(WebSocketCloseStatus normalClosure, string connectionClosedByTheServer, CancellationToken cancellationToken) =>
            _webSocket.CloseAsync(normalClosure, connectionClosedByTheServer, cancellationToken);

        public Task SendAsync(byte[] bytes, WebSocketMessageType binary, bool endOfMessage, CancellationToken cancellationToken) =>
            _webSocket.SendAsync(bytes, binary, endOfMessage, cancellationToken);

        public Task SendAsync(ArraySegment<byte> bytes, WebSocketMessageType binary, bool endOfMessage, CancellationToken cancellationToken) =>
            _webSocket.SendAsync(bytes, binary, endOfMessage, cancellationToken);

        public Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) =>
            _webSocket.CloseOutputAsync(closeStatus, statusDescription, cancellationToken);

        public bool IsOpen => _webSocket.State == WebSocketState.Open;
    }

    private class VoiceNetworking : IDisposable
    {
        private bool _disposed;
        private readonly ConfigurationFile _config;
        private readonly Channel<VoiceSnapshot> _snapshotSink = Channel.CreateUnbounded<VoiceSnapshot>();

        private readonly CancellationToken _shutDownToken;
        private readonly Uri _voiceUri;

        private const string WebSocketUri = "wss://voice.toxvoice.com:2096/voice-sink";

        public VoiceNetworking(ConfigurationFile config, CancellationToken shutdownToken)
        {
            _config = config;
            _shutDownToken = shutdownToken;
            _voiceUri = new Uri(WebSocketUri);
        }

        public async Task StartAsync()
        {
            using var sink = new TranscriptionLogSink(_config, _shutDownToken);
            while (!_shutDownToken.IsCancellationRequested)
            {
                var disconnectCts = CancellationTokenSource.CreateLinkedTokenSource(_shutDownToken);
                var retryDelay = TimeSpan.FromSeconds(2);

                try
                {
                    var connectResult = await WebSocketConnector.ConnectAsync(_voiceUri, _config.ToxVoice.Token, _shutDownToken).ConfigureAwait(false);
                    if (!connectResult.IsSuccess)
                    {
                        switch (connectResult.Error.Kind)
                        {
                            case ConnectionErrorKind.Fatal:
                                UnloadPlugin($"Handshake failed ({connectResult.Error.Message}). Unloading plugin.");
                                return;

                            case ConnectionErrorKind.InsufficientCredits:
                                Logger.Warning($"{connectResult.Error.Message} - Retrying in 10 minutes.");
                                retryDelay = TimeSpan.FromMinutes(10);
                                break;

                            case ConnectionErrorKind.Transient:
                                Logger.Warning("Failed to connect to ToxVoice. Retrying...");
                                break;
                        }

                        continue;
                    }

                    using var webSocket = connectResult.Value;
                    try
                    {
                        var sendTask = SendCog(webSocket, disconnectCts.Token).ContinueWith(_ =>
                        {
                            if (!disconnectCts.IsCancellationRequested)
                                disconnectCts.Cancel();
                        }, CancellationToken.None);

                        var receiveTask = ReceiveCog(webSocket, sink, disconnectCts.Token).ContinueWith(_ =>
                        {
                            if (!disconnectCts.IsCancellationRequested)
                                disconnectCts.Cancel();
                        }, CancellationToken.None);

                        Logger.Info("Connected");
                        await Task.WhenAll(sendTask, receiveTask).ConfigureAwait(false);
                    }
                    catch (OperationCanceledException)
                    { }
                    catch (Exception ex)
                    {
                        Logger.Error($"Unhandled Exception occurred: {ex.Message}");
                    }
                    finally
                    {
                        try
                        {
                            if (webSocket.IsOpen)
                            {
                                using var closeCts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
                                await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Plugin unloading", closeCts.Token)
                                    .ConfigureAwait(false);
                            }
                        }
                        catch
                        {
                            // Best-effort graceful close
                        }

                        Logger.Info("Disconnected");
                    }
                }
                catch (OperationCanceledException)
                {
                    return;
                }
                catch (Exception ex)
                {
                    Logger.Warning($"Unexpected connection error: {ex.Message}");
                }
                finally
                {
                    disconnectCts.Dispose();
                    await Task.Delay(retryDelay).ConfigureAwait(false);
                }
            }
        }

        private async Task ReceiveCog(ToxVoiceWebSocket webSocket, TranscriptionLogSink sink, CancellationToken cancellationToken)
        {
            var buffer = new byte[1024 * 16];
            using var ms = new MemoryStream();

            while (!cancellationToken.IsCancellationRequested)
            {
                ms.SetLength(0);
                try
                {
                    WebSocketReceiveResult receiveResult;

                    do
                    {
                        receiveResult = await webSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false);
                        ms.Write(buffer, 0, receiveResult.Count);
                    } while (!receiveResult.EndOfMessage);

                    if (receiveResult.MessageType == WebSocketMessageType.Close)
                    {
                        await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Connection closed by the server.", cancellationToken).ConfigureAwait(false);
                        break;
                    }

                    ms.Position = 0;

                    while (ms.Position < ms.Length)
                    {
                        var transcriptionResult = DeserializeTranscriptionResult(ms);
                        sink.TryWrite(transcriptionResult);
                    }
                }
                catch (OperationCanceledException)
                {
                    break;
                }
                catch (Exception exception)
                {
                    if (cancellationToken.IsCancellationRequested)
                        break;

                    Logger.Error($"WebSocket Receive Exception: {exception.Message}");
                    break;
                }
            }
        }

        private static TranscriptionResult DeserializeTranscriptionResult(MemoryStream ms)
        {
            using var reader = new BinaryReader(ms, Encoding.UTF8, true);

            var result = SharedObjectPool<TranscriptionResult>.Get();
            var metadataCount = reader.ReadInt32();

            for (var i = 0; i < metadataCount; i++)
            {
                var key = reader.ReadString();
                var value = reader.ReadString();
                result.Metadata[key] = value;
            }

            var audioDataLength = reader.ReadInt32();
            result.AudioData = reader.ReadBytes(audioDataLength);

            return result;
        }

        private async Task SendCog(ToxVoiceWebSocket webSocket, CancellationToken cancellationToken)
        {
            using var ms = new MemoryStream(20000);

            while (!cancellationToken.IsCancellationRequested)
            {
                try
                {
                    while (await _snapshotSink.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false))
                    {
                        while (_snapshotSink.Reader.TryRead(out var snapshot))
                        {
                            try
                            {
                                await WriteSnapshotBytes(ms, snapshot, cancellationToken).ConfigureAwait(false);

                                if (ms.Length > 0 && ms.TryGetBuffer(out var buffer))
                                {
                                    await webSocket.SendAsync(buffer, WebSocketMessageType.Binary, true, cancellationToken).ConfigureAwait(false);
                                }
                            }
                            catch (OperationCanceledException)
                            {
                                return;
                            }
                            catch (WebSocketException ex)
                            {
                                Logger.Warning($"WebSocket send failed: {ex.Message}");
                                return;
                            }
                            catch (Exception exception)
                            {
                                if (cancellationToken.IsCancellationRequested)
                                    return;

                                Logger.Warning($"Error processing voice snapshot, skipping: {exception.Message}");
                            }
                            finally
                            {
                                SharedObjectPool<VoiceSnapshot>.Return(snapshot);
                            }
                        }
                    }
                }
                catch (ChannelClosedException)
                {
                    break;
                }
            }
        }

        private static async Task WriteSnapshotBytes(MemoryStream reusableStream, VoiceSnapshot snapshot, CancellationToken cancellationToken)
        {
            reusableStream.SetLength(0);

            var currentPosition = string.Empty;
            RecipientList? recipientList = null;

            try
            {
                if (snapshot.ProximityLogs)
                {
                    recipientList = await InvokeMainThreadAsync(() =>
                    {
                        var player = RelationshipManager.FindByID(snapshot.UserId);
                        if (player is null)
                            return default;

                        currentPosition = player.transform.position.ToString();
                        return SphereHelper.GetRecipientsWithin(player);
                    }).ConfigureAwait(false);
                }
                else
                {
                    currentPosition = await InvokeMainThreadAsync(() =>
                    {
                        var player = RelationshipManager.FindByID(snapshot.UserId);
                        return player?.transform.position.ToString() ?? string.Empty;
                    }).ConfigureAwait(false);
                }
            }
            catch (TimeoutException)
            {
                // Main thread busy - send voice data without position metadata
            }

            var metadata = snapshot.Metadata;

            if (recipientList is { Items.Count: > 0 })
            {
                var items = recipientList.Items;
                items.Sort((a, b) => a.Distance.CompareTo(b.Distance));
                if (items.Count > 5)
                {
                    for (var i = 5; i < items.Count; i++)
                        SharedObjectPool<RecipientInfo>.Return(items[i]);

                    items.RemoveRange(5, items.Count - 5);
                }

                metadata["Recipients"] = BuildRecipientsJson(items);
            }

            SharedObjectPool<RecipientList>.Return(recipientList);

            if (!string.IsNullOrEmpty(currentPosition))
            {
                metadata["Position"] = currentPosition;
            }

            await using var binaryWriter = new BinaryWriter(reusableStream, Encoding.UTF8, true);
            binaryWriter.SerializeMetadata(metadata);
            binaryWriter.SerializeVoice(snapshot.VoicePackets);
        }

        private static string BuildRecipientsJson(List<RecipientInfo>? positions)
        {
            if (positions == null || positions.Count == 0)
            {
                return "[]";
            }

            var jsonBuilder = new StringBuilder(1024);
            jsonBuilder.Append('[');

            for (var i = 0; i < positions.Count; i++)
            {
                if (i > 0) jsonBuilder.Append(',');

                var recipient = positions[i];
                jsonBuilder.Append("{\"UserId\":")
                    .Append(recipient.UserId)
                    .Append(",\"DisplayName\":")
                    .Append(JsonConvert.ToString(recipient.DisplayName))
                    .Append(",\"Distance\":")
                    .Append(recipient.Distance.ToString("F2"))
                    .Append(",\"Timestamp\":")
                    .Append(recipient.Timestamp)
                    .Append('}');
            }

            jsonBuilder.Append(']');

            return jsonBuilder.ToString();
        }

        private static async Task<T> InvokeMainThreadAsync<T>(Func<T> action, int timeoutMs = 1000)
        {
            var cts = new TaskCompletionSource<T>();
            using var cancellationTokenSource = new CancellationTokenSource(timeoutMs);

            cancellationTokenSource.Token.Register(() => cts.TrySetCanceled(), false);

            Threading.QueueOnMainThread(() =>
            {
                try
                {
                    var result = action();
                    cts.TrySetResult(result);
                }
                catch (Exception ex)
                {
                    cts.TrySetException(ex);
                }
            });

            try
            {
                return await cts.Task.ConfigureAwait(false);
            }
            catch (TaskCanceledException)
            {
                throw new TimeoutException($"Operation timed out after {timeoutMs}ms");
            }
        }

        public void Dispose()
        {
            if (_disposed)
                return;

            _disposed = true;
            _snapshotSink.Writer.TryComplete();
        }

        public void TryWrite(VoiceSnapshot snapshot)
        {
            _snapshotSink.Writer.TryWrite(snapshot);
        }
    }

    private static class WebSocketConnector
    {
        public static async Task<Result<ToxVoiceWebSocket, ConnectionError>> ConnectAsync(Uri uri, string token, CancellationToken cancellationToken)
        {
            var addresses = await DnsResolver.ResolveHostnameAsync(uri.Host);

            while (!cancellationToken.IsCancellationRequested)
            {
                foreach (var ipAddress in addresses)
                {
                    cancellationToken.ThrowIfCancellationRequested();

                    try
                    {
                        var result = await ConnectToAddressAsync(uri, ipAddress, token, cancellationToken);
                        if (!result.IsSuccess)
                        {
                            if (result.Error.Kind != ConnectionErrorKind.Transient)
                                return result;

                            continue;
                        }

                        return result;
                    }
                    catch (OperationCanceledException)
                    {
                        throw;
                    }
                    catch (Exception)
                    { }
                }

                Logger.Warning("Failed to connect. Retrying...");
                await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
            }

            return Result<ToxVoiceWebSocket, ConnectionError>.Fail(ConnectionError.Transient("Cancelled"));
        }

        private static async Task<Result<ToxVoiceWebSocket, ConnectionError>> ConnectToAddressAsync(Uri uri, IPAddress ipAddress, string token, CancellationToken cancellationToken)
        {
            var socket = SocketFactory.CreateSocket(ipAddress.AddressFamily);
            await socket.ConnectAsync(ipAddress, uri.Port);

            var streamResult = await StreamFactory.CreateAndHandshakeStreamAsync(uri, socket, token);
            if (!streamResult.IsSuccess)
                return Result<ToxVoiceWebSocket, ConnectionError>.Fail(streamResult.Error);

            var stream = streamResult.Value;
            if (cancellationToken.IsCancellationRequested)
            {
                await stream.DisposeAsync().ConfigureAwait(false);
                throw new OperationCanceledException();
            }

            var webSocket = WebSocket.CreateFromStream(stream, false, null, TimeSpan.Zero);
            var toxVoiceWebSocket = new ToxVoiceWebSocket(webSocket, stream, socket);
            return Result<ToxVoiceWebSocket, ConnectionError>.Ok(toxVoiceWebSocket);
        }
    }

    private static class DnsResolver
    {
        public static async Task<IPAddress[]> ResolveHostnameAsync(string hostname)
        {
            var addresses = await Dns.GetHostAddressesAsync(hostname);
            var ipv4Addresses = addresses.Where(addr => addr.AddressFamily == AddressFamily.InterNetwork).ToArray();

            if (ipv4Addresses.Length == 0)
            {
                throw new Exception($"Unable to resolve hostname to IPv4 address: {hostname}");
            }

            return ipv4Addresses;
        }
    }

    private static class SocketFactory
    {
        public static Socket CreateSocket(AddressFamily addressFamily) => addressFamily == AddressFamily.InterNetworkV6
            ? new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp)
            : new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    }

    private static class StreamFactory
    {
        public static async Task<Result<Stream, ConnectionError>> CreateAndHandshakeStreamAsync(Uri uri, Socket socket, string token)
        {
            Stream? stream = null;
            try
            {
                stream = await CreateStreamAsync(uri, socket);
                var handshakeResult = await PerformHandshakeAsync(stream, uri, token);
                if (!handshakeResult.IsSuccess)
                {
                    if (stream is not null)
                        await stream.DisposeAsync();

                    return Result<Stream, ConnectionError>.Fail(handshakeResult.Error);
                }

                return Result<Stream, ConnectionError>.Ok(stream);
            }
            catch (Exception)
            {
                if (stream is not null)
                    await stream.DisposeAsync();

                throw;
            }
        }

        private static async Task<Stream> CreateStreamAsync(Uri uri, Socket socket)
        {
            if (uri.Scheme.ToLower() != "wss")
                return new NetworkStream(socket, true);

            var sslStream = new SslStream(new NetworkStream(socket, true), false, (_, _, _, _) => true);
            await sslStream.AuthenticateAsClientAsync(uri.Host);
            return sslStream;
        }

        private static async Task<Result<bool, ConnectionError>> PerformHandshakeAsync(Stream stream, Uri uri, string token)
        {
            var handshakeRequest = CreateHandshakeRequest(uri, token);
            var requestBytes = Encoding.ASCII.GetBytes(handshakeRequest);
            await stream.WriteAsync(requestBytes, 0, requestBytes.Length);

            var responseBuffer = new byte[4096];
            var bytesRead = await stream.ReadAsync(responseBuffer, 0, responseBuffer.Length);
            var response = Encoding.ASCII.GetString(responseBuffer, 0, bytesRead);

            if (response.Contains("101 Switching Protocols"))
                return Result<bool, ConnectionError>.Ok(true);

            var errorMessage = ExtractErrorMessage(response);

            if (errorMessage.Contains("Invalid") && errorMessage.Contains("Token"))
                return Result<bool, ConnectionError>.Fail(ConnectionError.Fatal("Invalid token"));

            if (errorMessage.Contains("No active subscription"))
                return Result<bool, ConnectionError>.Fail(ConnectionError.Fatal("No active subscription"));

            if (errorMessage.Contains("Trial period has expired"))
                return Result<bool, ConnectionError>.Fail(ConnectionError.Fatal("Trial period has expired"));

            if (errorMessage.Contains("Insufficient credits"))
                return Result<bool, ConnectionError>.Fail(ConnectionError.Credits());

            return Result<bool, ConnectionError>.Fail(ConnectionError.Transient("Handshake failed: " + errorMessage));
        }

        private static string ExtractErrorMessage(string response)
        {
            var lines = response.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
            for (var i = lines.Length - 1; i >= 0; i--)
            {
                var line = lines[i].Trim();
                if (!string.IsNullOrEmpty(line) &&
                    !line.StartsWith("HTTP/") &&
                    !line.Contains(": ") &&
                    !IsHexString(line))
                {
                    return line;
                }
            }

            return "Unknown error";
        }

        private static bool IsHexString(string str)
        {
            return !string.IsNullOrEmpty(str) && str.All(c => "0123456789abcdefABCDEF".Contains(c));
        }

        private static string CreateHandshakeRequest(Uri uri, string token) =>
            $"GET {uri.PathAndQuery} HTTP/1.1\r\n" +
            $"Host: {uri.Host}:{uri.Port}\r\n" +
            "Upgrade: websocket\r\n" +
            "Connection: Upgrade\r\n" +
            "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" +
            "Sec-WebSocket-Version: 13\r\n" +
            $"Token: {token}\r\n" +
            $"GameserverIp: {ConVar.Server.ip}:{ConVar.Server.port}\r\n" +
            "\r\n";
    }
}
public static class TranscriptionExtensions
{
    public static string GetString(this ToxVoice.TranscriptionResult transcriptionResult, string key) => !transcriptionResult.Metadata.TryGetValue(key, out var str) ? string.Empty : str;
}

public static class VoiceContextSerializer
{
    public static void SerializeMetadata(this BinaryWriter writer, Dictionary<string, string> metadata)
    {
        writer.Write((uint)metadata.Count);

        foreach (var kvp in metadata)
        {
            writer.Write(kvp.Key);
            writer.Write(kvp.Value);
        }
    }

    public static void SerializeVoice(this BinaryWriter writer, List<ToxVoice.VoicePacket> voicePackets)
    {
        writer.Write((uint)voicePackets.Count);
        foreach (var bytes in voicePackets)
        {
            if (bytes.Data.Length <= 12)
            {
                continue;
            }

            var newLength = bytes.Data.Length - 12;
            writer.Write(newLength);
            writer.Write(bytes.Data, 8, newLength);
        }
    }
}
 
